Azure Verified Modules

New content: AI-Assisted IaC Solution Development

New articles available on specification-driven development with AVM modules! Learn how to use GitHub Copilot and Spec Kit to accelerate the development of Azure infrastructure solutions with AVM. See end-to-end examples for both Bicep and Terraform.

Introduction

Value Proposition

Azure Verified Modules (AVM) is an initiative to consolidate and set the standards for what a good Infrastructure-as-Code module looks like.

Modules will then align to these standards, across languages (Bicep, Terraform etc.) and will then be classified as AVMs and available from their respective language specific registries.

AVM is a common code base, a toolkit for our Customers, our Partners, and Microsoft. It’s an official, Microsoft driven initiative, with a devolved ownership approach to develop modules, leveraging internal & external communities.

Azure Verified Modules enable and accelerate consistent solution development and delivery of cloud-native or migrated applications and their supporting infrastructure by codifying Microsoft guidance (WAF), with best practice configurations.

AVM development cycle

Modules

AVM module classificationsAzure Verified Modules provides two types of modules: Resource and Pattern modules.

AVM modules are used to deploy Azure resources and their extensions, as well as reusable architectural patterns consistently.

Modules are composable building blocks that encapsulate groups of resources dedicated to one task.

  • Flexible, generalized, multi-purpose
  • Integrates child resources
  • Integrates extension resources

AVM improves code quality and provides a unified customer experience.

Important

AVM is owned, developed & supported by Microsoft, you may raise a GitHub issue on this repository or the module’s repository directly to get support or log feature requests.

You can also log a support ticket and if the issue is not related to the Azure platform, you will be redirected to submit a GitHub issue for the module owner(s) or the AVM team.

See Module Support for more information.

Next Steps

  1. Review Overview

  2. Review the Module Classification Definitions

  3. Review the Specifications

  4. Review the FAQ

  5. Learn how to contribute to AVM

AVM AVM

Subsections of Azure Verified Modules

Subsections of Overview

Introduction

What is Azure Verified Modules?

Azure Verified Modules (AVM), as “One Microsoft”, we want to provide and define the single definition of what a good IaC module is;

  • How they should be constructed and built
    • Enforcing consistency and testing where possible
  • How they are to be consumed
  • What they deliver for consumers in terms of resources deployed and configured
  • And where appropriate aligned across IaC languages (e.g. Bicep, Terraform, etc.).
Mission Statement

Our mission is to deliver a comprehensive Azure Verified Modules library in multiple IaC languages, following the principles of the well-architected framework, serving as the trusted Microsoft source of truth. Supported by Microsoft, AVM will accelerate deployment time for Azure resources and architectural patterns, empowering every person and organization on the planet on their IaC journey.

Definition of “Verified” Summary

  • The modules are supported by Microsoft, across it’s many internal organizations, as described in Module Support
  • Modules are aligned to clear specifications that enforces consistency between all AVM modules. See the ‘Specifications & Definitions’ section in the menu
  • Modules will continue to stay up-to-date with product/service roadmaps owned by the module owners and contributors
  • Modules will align to WAF high priority recommendations. See ‘What does AVM mean by “WAF Aligned”?’
  • Modules will provide clear documentation alongside examples to promote self-service consumption
  • Modules will be tested to ensure they comply with the specifications for AVM and their examples deploy as intended

Why Azure Verified Modules?

This effort to create Azure Verified Modules, with a strategy and definition, is required based on the sheer number of existing attempts from all areas across Microsoft to try and address this same area for our customers and partners. Across Microsoft there are many initiatives, projects and repositories that host and provide IaC modules in several languages, for example Bicep and Terraform. Each of these come with differing code styling and standards, consumption methods and approaches, testing frameworks, target personas, contribution guidelines, module definitions and most importantly support statements from their owners and maintainers.

However, none of these existing attempts have ever made it all the way through to becoming a brand and the go to place for IaC modules from Microsoft that consumers can trust (mainly around longevity and support), build upon and contribute back to.

Performing this effort now to create a shared single aligned strategy and definition for IaC modules from Microsoft, as One Microsoft, will allow us to accelerate existing and future projects, such as Application Landing Zone Accelerators (LZAs), as well as providing the building blocks via a library of modules, in the language of the consumers choice, that is consistent, trusted and supported by Microsoft. This all leads to consumers being able to accelerate faster, no matter what stage of their IaC journey they are on.

We also know, from our customers, that well defined support statements from Microsoft are required for initiatives like this to succeed at scale, especially in larger enterprise customers. We have seen over the past FY that this topic alone is important and is one that has led to confusion and frustration to customers who are consuming modules developed by individuals that in the end are not “officially” Microsoft supported and this unfortunately normally occurs at a critical point in time for the project being worked on, which amplifies frustrations.

How will we create, support and enforce Azure Verified Modules?

Azure Verified Modules will achieve this, and its mission statement, by implementing and enforcing the following; driven by the AVM Core Team:

  1. Publishing AVM modules to their respective public registries for consumption
  2. Creating, publishing and maintaining the Azure Verified Modules specifications (this site)
    • Including IaC language specific specifications (today Bicep and Terraform)
  3. Creating easy to follow AVM module contribution and publishing guidance for each IaC language (today Bicep and Terraform)
  4. Enforcing tests for each AVM module is compliant with the AVM specifications, where possible, via Unit and Integration tests
  5. Enforcing End-to-End Deployment tests of each AVM module
  6. Providing, and backing, a long-term support statement, regardless of the AVM module’s ownership status
    • Backed by the AVM Core Team, Microsoft CSS and Azure PGs

Module Indexes

Summary

The following table shows the number of all available, orphaned and planned AVM Bicep and Terraform Modules.

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
BicepResource17023193
Pattern412162
Utility112
TerraformResource11042152
Pattern252651
Utility11314
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Want to contribute to AVM modules?
#
Labels
Link and description
1.Type: New Module Proposal πŸ’‘
Needs: Module Owner πŸ“£
To become the owner of a new module, see all new modules looking for owners or check out the “Looking for owners” swimlane here.
2.Status: Module Orphaned 🟑To become the owner of an orphaned module, see all orphaned modules or check out the “Orphaned” swimlane here.
3.Needs: Module Contributor πŸ“£To become a co-owner or contribute to a module, see all modules looking for contributors.

For more details on “What are the different ways to contribute to AVM?”, see here.

Subsections of Module Indexes

Bicep Modules

Summary

The following table shows the number of all available, orphaned and planned Bicep Modules.

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
BicepResource17023193
Pattern412162
Utility112
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Want to contribute to AVM Bicep modules?
#LabelsLink and description
1.Type: New Module Proposal πŸ’‘
Needs: Module Owner πŸ“£
Language: Bicep πŸ’ͺ
To become the owner of a new Bicep module, see all new Bicep modules looking for owners or check out the “Looking for owners” swimlane here.
2.Status: Module Orphaned 🟑 Language: Bicep πŸ’ͺTo become the owner of an orphaned Bicep module, see all orphaned Bicep modules or check out the “Orphaned” swimlane here.
3.Needs: Module Contributor πŸ“£ Language: Bicep πŸ’ͺTo become a co-owner or contribute to a Bicep module, see all Bicep modules looking for contributors.

For more details on “What are the different ways to contribute to AVM?”, see here.

Status Badges

This section gives you an overview of the latest workflow status of each AVM module in the Public Bicep Registry repository.

Note

While some pipelines can momentarily show as red, a new module version cannot be published without a successful test run. A failing test may indicate a recent change to the platform that is causing a break in the module or any intermittent errors, such as a periodic test deployment attempting to create a resource with a name already taken in another Azure region.

#ModuleStatus
1ptn/aca-lza

hosting-environment

avm.ptn.aca-lza.hosting-environment avm.ptn.aca-lza.hosting-environment
2ptn/ai-ml

ai-foundry

avm.ptn.ai-ml.ai-foundry avm.ptn.ai-ml.ai-foundry
3ptn/ai-platform

baseline

avm.ptn.ai-platform.baseline avm.ptn.ai-platform.baseline
4ptn/alz

ama

avm.ptn.alz.ama avm.ptn.alz.ama
5ptn/alz

empty

avm.ptn.alz.empty avm.ptn.alz.empty
6ptn/app-service-lza

hosting-environment

avm.ptn.app-service-lza.hosting-environment avm.ptn.app-service-lza.hosting-environment
7ptn/app

container-job-toolkit

avm.ptn.app.container-job-toolkit.yml avm.ptn.app.container-job-toolkit.yml
8ptn/app

iaas-vm-cosmosdb-tier4

avm.ptn.app.iaas-vm-cosmosdb-tier4 avm.ptn.app.iaas-vm-cosmosdb-tier4
9ptn/app

paas-ase-cosmosdb-tier4

avm.ptn.app.paas-ase-cosmosdb-tier4 avm.ptn.app.paas-ase-cosmosdb-tier4
10ptn/authorization

pim-role-assignment

avm.ptn.authorization.pim-role-assignment avm.ptn.authorization.pim-role-assignment
11ptn/authorization

policy-assignment

avm.ptn.authorization.policy-assignment avm.ptn.authorization.policy-assignment
12ptn/authorization

policy-exemption

avm.ptn.authorization.policy-exemption avm.ptn.authorization.policy-exemption
13ptn/authorization

resource-role-assignment

avm.ptn.authorization.resource-role-assignment avm.ptn.authorization.resource-role-assignment
14ptn/authorization

role-assignment

avm.ptn.authorization.role-assignment avm.ptn.authorization.role-assignment
15ptn/authorization

role-definition

avm.ptn.authorization.role-definition avm.ptn.authorization.role-definition
16ptn/azd

acr-container-app

avm.ptn.azd.acr-container-app avm.ptn.azd.acr-container-app
17ptn/azd

aks

avm.ptn.azd.aks avm.ptn.azd.aks
18ptn/azd

aks-automatic-cluster

avm.ptn.azd.aks-automatic-cluster avm.ptn.azd.aks-automatic-cluster
19ptn/azd

apim-api

avm.ptn.azd.apim-api avm.ptn.azd.apim-api
20ptn/azd

container-app-upsert

avm.ptn.azd.container-app-upsert avm.ptn.azd.container-app-upsert
21ptn/azd

container-apps-stack

avm.ptn.azd.container-apps-stack avm.ptn.azd.container-apps-stack
22ptn/azd

insights-dashboard

avm.ptn.azd.insights-dashboard avm.ptn.azd.insights-dashboard
23ptn/azd

ml-ai-environment

Deprecated
24ptn/azd

ml-hub-dependencies

Deprecated
25ptn/azd

ml-project

Deprecated
26ptn/azd

monitoring

avm.ptn.azd.monitoring avm.ptn.azd.monitoring
27ptn/data

private-analytical-workspace

avm.ptn.data.private-analytical-workspace avm.ptn.data.private-analytical-workspace
28ptn/deployment-script

import-image-to-acr

avm.ptn.deployment-script.import-image-to-acr avm.ptn.deployment-script.import-image-to-acr
29ptn/dev-ops

cicd-agents-and-runners

avm.ptn.dev-ops.cicd-agents-and-runners avm.ptn.dev-ops.cicd-agents-and-runners
30ptn/finops-toolkit

finops-hub

avm.ptn.finops-toolkit.finops-hub avm.ptn.finops-toolkit.finops-hub
31ptn/lz

sub-vending

avm.ptn.lz.sub-vending avm.ptn.lz.sub-vending
32ptn/mgmt-groups

subscription-placement

avm.ptn.mgmt-groups.subscription-placement avm.ptn.mgmt-groups.subscription-placement
33ptn/network

hub-networking

avm.ptn.network.hub-networking avm.ptn.network.hub-networking
34ptn/network

private-link-private-dns-zones

avm.ptn.network.private-link-private-dns-zones avm.ptn.network.private-link-private-dns-zones
35ptn/network

virtual-wan

avm.ptn.network.virtual-wan avm.ptn.network.virtual-wan
36ptn/policy-insights

remediation

avm.ptn.policy-insights.remediation avm.ptn.policy-insights.remediation
37ptn/sa

build-your-own-copilot

Deprecated
38ptn/sa

chat-with-your-data

avm.ptn.sa.chat-with-your-data avm.ptn.sa.chat-with-your-data
39ptn/sa

content-processing

avm.ptn.sa.content-processing avm.ptn.sa.content-processing
40ptn/sa

conversation-knowledge-mining

avm.ptn.sa.conversation-knowledge-mining avm.ptn.sa.conversation-knowledge-mining
41ptn/sa

customer-chatbot

avm.ptn.sa.customer-chatbot avm.ptn.sa.customer-chatbot
42ptn/sa

document-knowledge-mining

avm.ptn.sa.document-knowledge-mining avm.ptn.sa.document-knowledge-mining
43ptn/sa

modernize-your-code

avm.ptn.sa.modernize-your-code avm.ptn.sa.modernize-your-code
44ptn/sa

multi-agent-custom-automation-engine

avm.ptn.sa.multi-agent-custom-automation-engine avm.ptn.sa.multi-agent-custom-automation-engine
45ptn/security

security-center

avm.ptn.security.security-center avm.ptn.security.security-center
46ptn/subscription

service-health-alerts

avm.ptn.subscription.service-health-alerts avm.ptn.subscription.service-health-alerts
47ptn/virtual-machine-images

azure-image-builder

avm.ptn.virtual-machine-images.azure-image-builder avm.ptn.virtual-machine-images.azure-image-builder
48res/aad

domain-service

avm.res.aad.domain-service avm.res.aad.domain-service
49res/alerts-management

action-rule

avm.res.alerts-management.action-rule avm.res.alerts-management.action-rule
50res/analysis-services

server

avm.res.analysis-services.server avm.res.analysis-services.server
51res/api-management

service

avm.res.api-management.service avm.res.api-management.service
52res/app-configuration

configuration-store

avm.res.app-configuration.configuration-store avm.res.app-configuration.configuration-store
53res/app

container-app

avm.res.app.container-app avm.res.app.container-app
54res/app

job

avm.res.app.job avm.res.app.job
55res/app

managed-environment

avm.res.app.managed-environment avm.res.app.managed-environment
56res/app

session-pool

avm.res.app.session-pool avm.res.app.session-pool
57res/authorization

policy-assignment

avm.res.authorization.policy-assignment avm.res.authorization.policy-assignment
58res/authorization

role-assignment

avm.res.authorization.role-assignment avm.res.authorization.role-assignment
59res/automation

automation-account

avm.res.automation.automation-account avm.res.automation.automation-account
60res/azure-stack-hci

cluster

avm.res.azure-stack-hci.cluster avm.res.azure-stack-hci.cluster
61res/azure-stack-hci

logical-network

avm.res.azure-stack-hci.logical-network avm.res.azure-stack-hci.logical-network
62res/azure-stack-hci

marketplace-gallery-image

avm.res.azure-stack-hci.marketplace-gallery-image avm.res.azure-stack-hci.marketplace-gallery-image
63res/azure-stack-hci

network-interface

avm.res.azure-stack-hci.network-interface avm.res.azure-stack-hci.network-interface
64res/azure-stack-hci

virtual-hard-disk

avm.res.azure-stack-hci.virtual-hard-disk avm.res.azure-stack-hci.virtual-hard-disk
65res/azure-stack-hci

virtual-machine-instance

avm.res.azure-stack-hci.virtual-machine-instance avm.res.azure-stack-hci.virtual-machine-instance
66res/batch

batch-account

avm.res.batch.batch-account avm.res.batch.batch-account
67res/cache

redis

avm.res.cache.redis avm.res.cache.redis
68res/cache

redis-enterprise

avm.res.cache.redis-enterprise avm.res.cache.redis-enterprise
69res/cdn

profile

avm.res.cdn.profile avm.res.cdn.profile
70res/cognitive-services

account

avm.res.cognitive-services.account avm.res.cognitive-services.account
71res/communication

communication-service

avm.res.communication.communication-service avm.res.communication.communication-service
72res/communication

email-service

avm.res.communication.email-service avm.res.communication.email-service
73res/compute

availability-set

avm.res.compute.availability-set avm.res.compute.availability-set
74res/compute

disk

avm.res.compute.disk avm.res.compute.disk
75res/compute

disk-encryption-set

avm.res.compute.disk-encryption-set avm.res.compute.disk-encryption-set
76res/compute

gallery

avm.res.compute.gallery avm.res.compute.gallery
77res/compute

image

avm.res.compute.image avm.res.compute.image
78res/compute

proximity-placement-group

avm.res.compute.proximity-placement-group avm.res.compute.proximity-placement-group
79res/compute

ssh-public-key

avm.res.compute.ssh-public-key avm.res.compute.ssh-public-key
80res/compute

virtual-machine

avm.res.compute.virtual-machine avm.res.compute.virtual-machine
81res/compute

virtual-machine-scale-set

avm.res.compute.virtual-machine-scale-set avm.res.compute.virtual-machine-scale-set
82res/consumption

budget

avm.res.consumption.budget avm.res.consumption.budget
83res/container-instance

container-group

avm.res.container-instance.container-group avm.res.container-instance.container-group
84res/container-registry

registry

avm.res.container-registry.registry avm.res.container-registry.registry
85res/container-service

managed-cluster

avm.res.container-service.managed-cluster avm.res.container-service.managed-cluster
86res/data-factory

factory

avm.res.data-factory.factory avm.res.data-factory.factory
87res/data-protection

backup-vault

avm.res.data-protection.backup-vault avm.res.data-protection.backup-vault
88res/databricks

access-connector

avm.res.databricks.access-connector avm.res.databricks.access-connector
89res/databricks

workspace

avm.res.databricks.workspace avm.res.databricks.workspace
90res/db-for-my-sql

flexible-server

avm.res.db-for-my-sql.flexible-server avm.res.db-for-my-sql.flexible-server
91res/db-for-postgre-sql

flexible-server

avm.res.db-for-postgre-sql.flexible-server avm.res.db-for-postgre-sql.flexible-server
92res/desktop-virtualization

application-group

avm.res.desktop-virtualization.application-group avm.res.desktop-virtualization.application-group
93res/desktop-virtualization

host-pool

avm.res.desktop-virtualization.host-pool avm.res.desktop-virtualization.host-pool
94res/desktop-virtualization

scaling-plan

avm.res.desktop-virtualization.scaling-plan avm.res.desktop-virtualization.scaling-plan
95res/desktop-virtualization

workspace

avm.res.desktop-virtualization.workspace avm.res.desktop-virtualization.workspace
96res/dev-center

devcenter

avm.res.dev-center.devcenter avm.res.dev-center.devcenter
97res/dev-center

network-connection

avm.res.dev-center.network-connection avm.res.dev-center.network-connection
98res/dev-center

project

avm.res.dev-center.project avm.res.dev-center.project
99res/dev-ops-infrastructure

pool

avm.res.dev-ops-infrastructure.pool avm.res.dev-ops-infrastructure.pool
100res/dev-test-lab

lab

avm.res.dev-test-lab.lab avm.res.dev-test-lab.lab
101res/devices

iot-hub

avm.res.devices.iot-hub avm.res.devices.iot-hub
102res/digital-twins

digital-twins-instance

avm.res.digital-twins.digital-twins-instance avm.res.digital-twins.digital-twins-instance
103res/document-db

database-account

avm.res.document-db.database-account avm.res.document-db.database-account
104res/document-db

mongo-cluster

avm.res.document-db.mongo-cluster avm.res.document-db.mongo-cluster
105res/elastic-san

elastic-san

avm.res.elastic-san.elastic-san avm.res.elastic-san.elastic-san
106res/event-grid

domain

avm.res.event-grid.domain avm.res.event-grid.domain
107res/event-grid

namespace

avm.res.event-grid.namespace avm.res.event-grid.namespace
108res/event-grid

system-topic

avm.res.event-grid.system-topic avm.res.event-grid.system-topic
109res/event-grid

topic

avm.res.event-grid.topic avm.res.event-grid.topic
110res/event-hub

namespace

avm.res.event-hub.namespace avm.res.event-hub.namespace
111res/fabric

capacity

avm.res.fabric.capacity avm.res.fabric.capacity
112res/health-bot

health-bot

avm.res.health-bot.health-bot avm.res.health-bot.health-bot
113res/healthcare-apis

workspace

avm.res.healthcare-apis.workspace avm.res.healthcare-apis.workspace
114res/hybrid-compute

gateway

avm.res.hybrid-compute.gateway avm.res.hybrid-compute.gateway
115res/hybrid-compute

license

avm.res.hybrid-compute.license avm.res.hybrid-compute.license
116res/hybrid-compute

machine

avm.res.hybrid-compute.machine avm.res.hybrid-compute.machine
117res/hybrid-container-service

provisioned-cluster-instance

avm.res.hybrid-container-service.provisioned-cluster-instance avm.res.hybrid-container-service.provisioned-cluster-instance
118res/insights

action-group

avm.res.insights.action-group avm.res.insights.action-group
119res/insights

activity-log-alert

avm.res.insights.activity-log-alert avm.res.insights.activity-log-alert
120res/insights

component

avm.res.insights.component avm.res.insights.component
121res/insights

data-collection-endpoint

avm.res.insights.data-collection-endpoint avm.res.insights.data-collection-endpoint
122res/insights

data-collection-rule

avm.res.insights.data-collection-rule avm.res.insights.data-collection-rule
123res/insights

diagnostic-setting

avm.res.insights.diagnostic-setting avm.res.insights.diagnostic-setting
124res/insights

metric-alert

avm.res.insights.metric-alert avm.res.insights.metric-alert
125res/insights

private-link-scope

avm.res.insights.private-link-scope avm.res.insights.private-link-scope
126res/insights

scheduled-query-rule

avm.res.insights.scheduled-query-rule avm.res.insights.scheduled-query-rule
127res/insights

webtest

avm.res.insights.webtest avm.res.insights.webtest
128res/key-vault

vault

avm.res.key-vault.vault avm.res.key-vault.vault
129res/kubernetes-configuration

extension

avm.res.kubernetes-configuration.extension avm.res.kubernetes-configuration.extension
130res/kubernetes-configuration

flux-configuration

avm.res.kubernetes-configuration.flux-configuration avm.res.kubernetes-configuration.flux-configuration
131res/kubernetes-runtime

load-balancer

avm.res.kubernetes-runtime.load-balancer avm.res.kubernetes-runtime.load-balancer
132res/kubernetes

connected-cluster

avm.res.kubernetes.connected-cluster avm.res.kubernetes.connected-cluster
133res/kusto

cluster

avm.res.kusto.cluster avm.res.kusto.cluster
134res/load-test-service

load-test

avm.res.load-test-service.load-test avm.res.load-test-service.load-test
135res/logic

integration-account

avm.res.logic.integration-account avm.res.logic.integration-account
136res/logic

workflow

avm.res.logic.workflow avm.res.logic.workflow
137res/machine-learning-services

registry

avm.res.machine-learning-services.registry avm.res.machine-learning-services.registry
138res/machine-learning-services

workspace

avm.res.machine-learning-services.workspace avm.res.machine-learning-services.workspace
139res/maintenance

configuration-assignment

avm.res.maintenance.configuration-assignment avm.res.maintenance.configuration-assignment
140res/maintenance

maintenance-configuration

avm.res.maintenance.maintenance-configuration avm.res.maintenance.maintenance-configuration
141res/managed-identity

user-assigned-identity

avm.res.managed-identity.user-assigned-identity avm.res.managed-identity.user-assigned-identity
142res/managed-services

registration-definition

avm.res.managed-services.registration-definition avm.res.managed-services.registration-definition
143res/management

management-group

avm.res.management.management-group avm.res.management.management-group
144res/management

service-group

avm.res.management.service-group avm.res.management.service-group
145res/maps

account

avm.res.maps.account avm.res.maps.account
146res/net-app

net-app-account

avm.res.net-app.net-app-account avm.res.net-app.net-app-account
147res/network

application-gateway

avm.res.network.application-gateway avm.res.network.application-gateway
148res/network

application-gateway-web-application-firewall-policy

avm.res.network.application-gateway-web-application-firewall-policy avm.res.network.application-gateway-web-application-firewall-policy
149res/network

application-security-group

avm.res.network.application-security-group avm.res.network.application-security-group
150res/network

azure-firewall

avm.res.network.azure-firewall avm.res.network.azure-firewall
151res/network

bastion-host

avm.res.network.bastion-host avm.res.network.bastion-host
152res/network

connection

avm.res.network.connection avm.res.network.connection
153res/network

ddos-protection-plan

avm.res.network.ddos-protection-plan avm.res.network.ddos-protection-plan
154res/network

dns-forwarding-ruleset

avm.res.network.dns-forwarding-ruleset avm.res.network.dns-forwarding-ruleset
155res/network

dns-resolver

avm.res.network.dns-resolver avm.res.network.dns-resolver
156res/network

dns-zone

avm.res.network.dns-zone avm.res.network.dns-zone
157res/network

express-route-circuit

avm.res.network.express-route-circuit avm.res.network.express-route-circuit
158res/network

express-route-gateway

avm.res.network.express-route-gateway avm.res.network.express-route-gateway
159res/network

express-route-port

avm.res.network.express-route-port avm.res.network.express-route-port
160res/network

firewall-policy

avm.res.network.firewall-policy avm.res.network.firewall-policy
161res/network

front-door

Deprecated
162res/network

front-door-web-application-firewall-policy

avm.res.network.front-door-web-application-firewall-policy avm.res.network.front-door-web-application-firewall-policy
163res/network

ip-group

avm.res.network.ip-group avm.res.network.ip-group
164res/network

load-balancer

avm.res.network.load-balancer avm.res.network.load-balancer
165res/network

local-network-gateway

avm.res.network.local-network-gateway avm.res.network.local-network-gateway
166res/network

nat-gateway

avm.res.network.nat-gateway avm.res.network.nat-gateway
167res/network

network-interface

avm.res.network.network-interface avm.res.network.network-interface
168res/network

network-manager

avm.res.network.network-manager avm.res.network.network-manager
169res/network

network-security-group

avm.res.network.network-security-group avm.res.network.network-security-group
170res/network

network-security-perimeter

avm.res.network.network-security-perimeter avm.res.network.network-security-perimeter
171res/network

network-watcher

avm.res.network.network-watcher avm.res.network.network-watcher
172res/network

p2s-vpn-gateway

avm.res.network.p2s-vpn-gateway avm.res.network.p2s-vpn-gateway
173res/network

private-dns-zone

avm.res.network.private-dns-zone avm.res.network.private-dns-zone
174res/network

private-endpoint

avm.res.network.private-endpoint avm.res.network.private-endpoint
175res/network

private-link-service

avm.res.network.private-link-service avm.res.network.private-link-service
176res/network

public-ip-address

avm.res.network.public-ip-address avm.res.network.public-ip-address
177res/network

public-ip-prefix

avm.res.network.public-ip-prefix avm.res.network.public-ip-prefix
178res/network

route-table

avm.res.network.route-table avm.res.network.route-table
179res/network

service-endpoint-policy

avm.res.network.service-endpoint-policy avm.res.network.service-endpoint-policy
180res/network

trafficmanagerprofile

avm.res.network.trafficmanagerprofile avm.res.network.trafficmanagerprofile
181res/network

virtual-hub

avm.res.network.virtual-hub avm.res.network.virtual-hub
182res/network

virtual-network

avm.res.network.virtual-network avm.res.network.virtual-network
183res/network

virtual-network-gateway

avm.res.network.virtual-network-gateway avm.res.network.virtual-network-gateway
184res/network

virtual-wan

avm.res.network.virtual-wan avm.res.network.virtual-wan
185res/network

vpn-gateway

avm.res.network.vpn-gateway avm.res.network.vpn-gateway
186res/network

vpn-server-configuration

avm.res.network.vpn-server-configuration avm.res.network.vpn-server-configuration
187res/network

vpn-site

avm.res.network.vpn-site avm.res.network.vpn-site
188res/operational-insights

cluster

avm.res.operational-insights.cluster avm.res.operational-insights.cluster
189res/operational-insights

workspace

avm.res.operational-insights.workspace avm.res.operational-insights.workspace
190res/operations-management

solution

avm.res.operations-management.solution avm.res.operations-management.solution
191res/portal

dashboard

avm.res.portal.dashboard avm.res.portal.dashboard
192res/power-bi-dedicated

capacity

avm.res.power-bi-dedicated.capacity avm.res.power-bi-dedicated.capacity
193res/purview

account

avm.res.purview.account avm.res.purview.account
194res/recovery-services

vault

avm.res.recovery-services.vault avm.res.recovery-services.vault
195res/relay

namespace

avm.res.relay.namespace avm.res.relay.namespace
196res/resource-graph

query

avm.res.resource-graph.query avm.res.resource-graph.query
197res/resources

deployment-script

avm.res.resources.deployment-script avm.res.resources.deployment-script
198res/resources

resource-group

avm.res.resources.resource-group avm.res.resources.resource-group
199res/search

search-service

avm.res.search.search-service avm.res.search.search-service
200res/security-insights

data-connector

avm.res.security-insights.data-connector avm.res.security-insights.data-connector
201res/security-insights

setting

avm.res.security-insights.setting avm.res.security-insights.setting
202res/service-bus

namespace

avm.res.service-bus.namespace avm.res.service-bus.namespace
203res/service-fabric

cluster

avm.res.service-fabric.cluster avm.res.service-fabric.cluster
204res/service-networking

traffic-controller

avm.res.service-networking.traffic-controller avm.res.service-networking.traffic-controller
205res/signal-r-service

signal-r

avm.res.signal-r-service.signal-r avm.res.signal-r-service.signal-r
206res/signal-r-service

web-pub-sub

avm.res.signal-r-service.web-pub-sub avm.res.signal-r-service.web-pub-sub
207res/sql

instance-pool

avm.res.sql.instance-pool avm.res.sql.instance-pool
208res/sql

managed-instance

avm.res.sql.managed-instance avm.res.sql.managed-instance
209res/sql

server

avm.res.sql.server avm.res.sql.server
210res/storage

storage-account

avm.res.storage.storage-account avm.res.storage.storage-account
211res/synapse

private-link-hub

avm.res.synapse.private-link-hub avm.res.synapse.private-link-hub
212res/synapse

workspace

avm.res.synapse.workspace avm.res.synapse.workspace
213res/virtual-machine-images

image-template

avm.res.virtual-machine-images.image-template avm.res.virtual-machine-images.image-template
214res/web

connection

avm.res.web.connection avm.res.web.connection
215res/web

hosting-environment

avm.res.web.hosting-environment avm.res.web.hosting-environment
216res/web

serverfarm

avm.res.web.serverfarm avm.res.web.serverfarm
217res/web

site

avm.res.web.site avm.res.web.site
218res/web

static-site

avm.res.web.static-site avm.res.web.static-site
219utl/types

avm-common-types

avm.utl.types.avm-common-types avm.utl.types.avm-common-types

Subsections of Bicep

Bicep Resource Modules

Module catalog

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
BicepResource17023193
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Info

This page contains various views of the module index (catalog) for Bicep Resource Modules. To see these views, click on the expandable sections with the “βž•” sign below.

  • To see the full, unfiltered, unformatted module index on GitHub, click here.

  • To download the source CSV file, click here.

Note

Modules listed below that aren’t shown with the status of Module Available 🟒, are currently in development and are not yet available for use. For proposed modules, see the Proposed modules section below.

Published modules - 🟒 & 🟑

βž• Published Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/aad/domain-serviceAzure Active Directory Domain Service
AAD, Entra ID, Microsoft Entra Domain Services, AAD DS, Azure AD DS
ReneHezser
Rene Hezser
CRYP70N1X
Paul Chirila
02avm/res/alerts-management/action-ruleAction Rulesjudyer28
Justin Dyer
03avm/res/analysis-services/serverAnalysis Services Server
04avm/res/api-management/service
API Management Service
05avm/res/app-configuration/configuration-store
  • key-value
  • replica
App Configuration StoreJfolberth
John Folberth
06avm/res/app/container-app
  • auth-config
Container AppoZakari
Zach Trocinski
07avm/res/app/jobApp JobReneHezser
Rene Hezser
08avm/res/app/managed-environment
  • certificate
  • storage
App Managed Environment
09avm/res/app/session-poolApp Session Pool
10avm/res/authorization/policy-assignment
Authorization - Policy AssignmentAlexanderSehr
Alexander Sehr
11avm/res/authorization/role-assignment
Authorization - Role Assignmentarnoldna
Nate Arnold
12avm/res/automation/automation-account
  • credential
  • hybrid-runbook-worker-group
    • hybrid-runbook-worker
  • job-schedule
  • module
  • powershell72-module
  • python2-package
  • python3-package
  • runbook
  • schedule
  • source-control
  • variable
  • webhook
Automation Accountgpacetti
Giuseppe Pacetti
13avm/res/azure-stack-hci/cluster
  • arc-setting/extension
  • deployment-setting
Azure Stack HCI Clusterchirag1603
Chirag Choudha
14avm/res/azure-stack-hci/logical-networkAzure Stack HCI Logical Networkchirag1603
Chirag Choudha
15avm/res/azure-stack-hci/marketplace-gallery-imageAzure Stack HCI Marketplace Gallery Imagechirag1603
Chirag Choudha
16avm/res/azure-stack-hci/network-interfaceAzure Stack HCI Network Interfacechirag1603
Chirag Choudha
17avm/res/azure-stack-hci/virtual-hard-diskAzure Stack HCI Hard Diskchirag1603
Chirag Choudha
18avm/res/azure-stack-hci/virtual-machine-instanceAzure Stack HCI Virtual Machine Instancechirag1603
Chirag Choudha
19avm/res/batch/batch-accountBatch Accountdidayal-msft
Divyadeep Dayal
20avm/res/cache/redis
  • access-policy
  • access-policy-assignment
  • firewall-rule
  • linked-server
Redis CacheTomazMlakar
Tomaz Mlakar
21avm/res/cache/redis-enterprise
  • database
    • access-policy-assignment
Redis Enterprise CacheJeffreyCA
Jeffrey Chen
22avm/res/cdn/profile
CDN Profile
Azure Front Door
gbeaud
Guillaume Beaud
23avm/res/cognitive-services/accountAzure AI Services (Cognitive Services)
AI Foundry
mswantek68
Mike Swantek
chanakanissanka
Chanaka Nissanka
24avm/res/communication/communication-serviceCommunication Servicedonk-msft
Don Koning
25avm/res/communication/email-service
  • domain
    • sender-username
Email Communication Servicedonk-msft
Don Koning
26avm/res/compute/availability-setAvailability Set
AS
ahmadabdalla
Ahmad Abdalla
27avm/res/compute/diskCompute Disksegraef
Sebastian Graef
28avm/res/compute/disk-encryption-setDisk Encryption Setsegraef
Sebastian Graef
29avm/res/compute/gallery
  • application
  • image
Azure Compute GalleryReneHezser
Rene Hezser
30avm/res/compute/imageImagetony-box
Tony Box
31avm/res/compute/proximity-placement-groupProximity Placement Groupjeetgarg
Jeet Garg
32avm/res/compute/ssh-public-keyPublic SSH KeyChrisSidebotham
Chris Sidebotham
33avm/res/compute/virtual-machine
  • extension
Virtual Machine
VM
josunefon
Jordi Sune Fontanals
34avm/res/compute/virtual-machine-scale-set
  • extension
Virtual Machine Scale Set
VMSS
josunefon
Jordi Sune Fontanals
35avm/res/consumption/budget
Consumption Budgetsegraef
Sebastian Graef
36avm/res/container-instance/container-groupContainer Instance
ACI
JPEasier
Julian Peißker
37avm/res/container-registry/registry
Azure Container Registry (ACR)JPEasier
Julian Peißker
38avm/res/container-service/managed-cluster
  • agent-pool
  • maintenance-configuration
Azure Kubernetes Service (AKS) Managed ClusterJPEasier
Julian Peißker
39avm/res/data-factory/factory
  • integration-runtime
  • linked-service
  • managed-virtual-network
    • managed-private-endpoint
Data FactoryTomazMlakar
Tomaz Mlakar
40avm/res/data-protection/backup-vault
  • backup-instance
  • backup-policy
Data Protection Backup Vault
41avm/res/databricks/access-connectorAzure Databricks Access Connector
42avm/res/databricks/workspaceAzure Databricks Workspace
43avm/res/db-for-my-sql/flexible-server
  • administrator
  • advanced-threat-protection
  • configuration
  • database
  • firewall-rule
DB for MySQL Flexible Server
44avm/res/db-for-postgre-sql/flexible-server
  • administrator
  • advanced-threat-protection-setting
  • configuration
  • database
  • firewall-rule
DB for Postgre SQL Flexible Serverarnoldna
Nate Arnold
45avm/res/desktop-virtualization/application-group
  • application
Azure Virtual Desktop (AVD) Application Group
46avm/res/desktop-virtualization/host-poolAzure Virtual Desktop (AVD) Host Pool
47avm/res/desktop-virtualization/scaling-planAzure Virtual Desktop (AVD) Scaling Plan
48avm/res/desktop-virtualization/workspaceAzure Virtual Desktop (AVD) Workspace
49avm/res/dev-center/devcenter
  • attachednetwork
  • catalog
  • devboxdefinition
  • environment-type
  • gallery
  • project-policy
Dev Centerahmadabdalla
Ahmad Abdalla
50avm/res/dev-center/network-connectionDev Center Network Connectionahmadabdalla
Ahmad Abdalla
51avm/res/dev-center/project
  • catalog
  • environment-type
  • pool
    • schedule
Dev Center Projectahmadabdalla
Ahmad Abdalla
52avm/res/dev-ops-infrastructure/poolDevOps Infrastructure Poolelizatargithub7
Eliza Tarasila
53avm/res/dev-test-lab/lab
  • artifactsource
  • cost
  • notificationchannel
  • policyset/policy
  • schedule
  • secret
  • virtualnetwork
DevTest Labahmadabdalla
Ahmad Abdalla
54avm/res/devices/iot-hubIoT HubTomazMlakar
Tomaz Mlakar
55avm/res/digital-twins/digital-twins-instance
  • endpoint
Digital Twins Instanceryanmstephens
Ryan Stephens
56avm/res/document-db/database-account
Cosmos DB Database Accountcmaneu
Christopher Maneu
57avm/res/document-db/mongo-cluster
  • firewall-rule
  • user
Cosmos DB for MongoDB (vCore)sinedied
Yohan Lasorsa
58avm/res/elastic-san/elastic-san
  • volume-group
    • snapshot
    • volume
Elastic SAN
SAN, ESAN, Elastic SAN, Azure Elastic Storage Area Network, iSCSI, internet Small Computer Systems Interface
jbinko
Jiri Binko
59avm/res/event-grid/domain
  • event-subscription
  • topic
Event Grid Domainfabmas
Fabio Masciotra
60avm/res/event-grid/namespace
  • ca-certificate
  • client
  • client-group
  • permission-binding
  • topic
    • event-subscription
  • topic-space
Event Grid Namespacefabmas
Fabio Masciotra
61avm/res/event-grid/system-topic
  • event-subscription
Event Grid System Topicfabmas
Fabio Masciotra
62avm/res/event-grid/topic
  • event-subscription
Event Grid Topicfabmas
Fabio Masciotra
63avm/res/event-hub/namespace
  • authorization-rule
  • disaster-recovery-config
  • eventhub
    • authorization-rule
    • consumergroup
  • network-rule-set
Event Hub Namespace
64avm/res/fabric/capacityFabricmswantek68
Mike Swantek
cmaneu
Christopher Maneu
65avm/res/health-bot/health-botAzure Health Bot
66avm/res/healthcare-apis/workspace
  • dicomservice
  • fhirservice
  • iotconnector
    • fhirdestination
Healthcare API Workspace
67avm/res/hybrid-compute/gatewayHybrid Compute Gatewaychirag1603
Chirag Choudha
68avm/res/hybrid-compute/licenseHybrid Compute Licensechirag1603
Chirag Choudha
69avm/res/hybrid-compute/machine
  • extension
Hybrid Compute Machinechirag1603
Chirag Choudha
70avm/res/hybrid-container-service/provisioned-cluster-instanceHybrid Container Service - Provisioned Cluster Instancechirag1603
Chirag Choudha
71avm/res/insights/action-groupAction Grouprahalan
Rainer Halanek
72avm/res/insights/activity-log-alertActivity Log Alertdonk-msft
Don Koning
73avm/res/insights/component
  • linked-storage-account
Application Insightkrbar
Kris Baranek
74avm/res/insights/data-collection-endpointData Collection Endpointkrbar
Kris Baranek
75avm/res/insights/data-collection-ruleData Collection Rule
DCR
krbar
Kris Baranek
76avm/res/insights/diagnostic-settingDiagnostic Settingkrbar
Kris Baranek
77avm/res/insights/metric-alertMetric Alertkijunkang
Ki Jun Kang
78avm/res/insights/private-link-scope
  • scoped-resource
Azure Monitor Private Link Scopeahmadabdalla
Ahmad Abdalla
79avm/res/insights/scheduled-query-ruleScheduled Query Rule
80avm/res/insights/webtestWeb TestJfolberth
John Folberth
81avm/res/key-vault/vault
Key Vault
KV
fblix
Felix Borst
82avm/res/kubernetes-configuration/extensionKubernetes Configuration ExtensionJPEasier
Julian Peißker
83avm/res/kubernetes-configuration/flux-configurationKubernetes Configuration Flux ConfigurationJPEasier
Julian Peißker
84avm/res/kubernetes-runtime/load-balancerKubernetes Runtime Load Balancerchirag1603
Chirag Choudha
85avm/res/kubernetes/connected-clusterKubernetes Connected Clusterchirag1603
Chirag Choudha
86avm/res/kusto/cluster
  • database
    • principal-assignment
  • principal-assignment
Azure Data Explorer (Kusto) clusteroZakari
Zach Trocinski
87avm/res/load-test-service/load-testLoad Testing Servicesebassem
Seif Bassem
88avm/res/logic/integration-account
  • agreement
  • assembly
  • certificate
  • map
  • partner
  • schema
Logic Apps Integration Accountlsnoddy
Luke Snoddy
89avm/res/logic/workflowLogic Apps Workflowlsnoddy
Luke Snoddy
90avm/res/machine-learning-services/registryMachine Learning Services Registryjosunefon
Jordi Sune Fontanals
91avm/res/machine-learning-services/workspace
  • compute
  • connection
  • datastore
Machine Learning Services Workspace
ML Workspace
cecheta
Chinedum Echeta
ross-p-smith
Ross Smith
92avm/res/maintenance/configuration-assignmentMaintenance Configuration Assignmenteriqua
Erika Gressi
93avm/res/maintenance/maintenance-configurationMaintenance Configurationarievanderwende
Arie van der Wende
94avm/res/managed-identity/user-assigned-identity
  • federated-identity-credential
User Assigned Identity
MSI
gpacetti
Giuseppe Pacetti
95avm/res/managed-services/registration-definitionRegistration Definition (Lighthouse)
96avm/res/management/management-groupManagement Group
MG
fblix
Felix Borst
97avm/res/management/service-groupService Groupjtracey93
Jack Tracey
98avm/res/maps/accountAzure Maps Accountjhueppauff
Julian Huppauff
99avm/res/net-app/net-app-account
  • backup-policy
  • backup-vault
    • backup
  • capacity-pool
    • volume
  • snapshot-policy
Azure NetApp Filefbinotto
Felipe Binotto
100avm/res/network/application-gatewayApplication Gateway
App GW
toddbeauchemin
Todd Beauchemin
101avm/res/network/application-gateway-web-application-firewall-policyApplication Gateway Web Application Firewall (WAF) Policytoddbeauchemin
Todd Beauchemin
102avm/res/network/application-security-groupApplication Security Group (ASG)
ASG
segraef
Sebastian Graef
103avm/res/network/azure-firewallAzure Firewall
Azure FW
jtracey93
Jack Tracey
oZakari
Zach Trocinski
104avm/res/network/bastion-hostBastion Hostkrbar
Kris Baranek
105avm/res/network/connectionVirtual Network Gateway Connectionfabmas
Fabio Masciotra
106avm/res/network/ddos-protection-planDDoS Protection Plansegraef
Sebastian Graef
107avm/res/network/dns-forwarding-ruleset
  • forwarding-rule
  • virtual-network-link
DNS Forwarding RulesetChrisSidebotham
Chris Sidebotham
108avm/res/network/dns-resolver
  • inbound-endpoint
  • outbound-endpoint
DNS ResolverChrisSidebotham
Chris Sidebotham
109avm/res/network/dns-zone
  • a
  • aaaa
  • caa
  • cname
  • mx
  • ns
  • ptr
  • soa
  • srv
  • txt
Public DNS ZoneChrisSidebotham
Chris Sidebotham
110avm/res/network/express-route-circuitExpressRoute Circuit
ER Circuit
arnoldna
Nate Arnold
111avm/res/network/express-route-gatewayExpress Route Gateway
ER GW
arnoldna
Nate Arnold
112avm/res/network/express-route-portExpressRoute Port
ER Port
arnoldna
Nate Arnold
113avm/res/network/firewall-policy
  • rule-collection-group
Firewall Policyjtracey93
Jack Tracey
oZakari
Zach Trocinski
114avm/res/network/front-door-web-application-firewall-policyFront Door Web Application Firewall (WAF) PolicyPaulJohnston88
Paul Johnston
115avm/res/network/ip-groupIP Groupahmadabdalla
Ahmad Abdalla
116avm/res/network/load-balancer
  • backend-address-pool
  • inbound-nat-rule
Load Balancer
LB, NLB
arnoldna
Nate Arnold
117avm/res/network/local-network-gatewayLocal Network Gatewayfabmas
Fabio Masciotra
118avm/res/network/nat-gatewayNAT Gateway
NAT GW
fabmas
Fabio Masciotra
119avm/res/network/network-interfaceNetwork Interface
NIC
rahalan
Rainer Halanek
120avm/res/network/network-manager
  • connectivity-configuration
  • network-group
    • static-member
  • routing-configuration
    • rule-collection
      • rule
  • scope-connection
  • security-admin-configuration
    • rule-collection
      • rule
Network Managerahmadabdalla
Ahmad Abdalla
121avm/res/network/network-security-groupNetwork Security Group
NSG
ahmadabdalla
Ahmad Abdalla
122avm/res/network/network-security-perimeter
  • profile
    • access-rule
Network Security Perimeterpeterbud
Peter Budai
123avm/res/network/network-watcher
  • connection-monitor
  • flow-log
Network Watchersegraef
Sebastian Graef
124avm/res/network/p2s-vpn-gatewayP2S VPN Gatewayericscheffler
Eric Scheffler
125avm/res/network/private-dns-zone
Private DNS ZoneChrisSidebotham
Chris Sidebotham
126avm/res/network/private-endpoint
  • private-dns-zone-group
Private Endpointsegraef
Sebastian Graef
127avm/res/network/private-link-servicePrivate Link Serviceahmadabdalla
Ahmad Abdalla
128avm/res/network/public-ip-addressPublic IP Address
PIP
ChrisSidebotham
Chris Sidebotham
krbar
Kris Baranek
129avm/res/network/public-ip-prefixPublic IP Prefix
PIP Prefix
krbar
Kris Baranek
130avm/res/network/route-tableRoute Table
UDR
segraef
Sebastian Graef
131avm/res/network/service-endpoint-policyService Endpoint Policyjeetgarg
Jeet Garg
132avm/res/network/trafficmanagerprofileTraffic Manager Profilelsnoddy
Luke Snoddy
133avm/res/network/virtual-hub
  • hub-route-table
  • hub-virtual-network-connection
  • route-map
  • routing-intent
Virtual Hubarnoldna
Nate Arnold
134avm/res/network/virtual-network
Virtual Network
VNET
mjrich19
MJ Richardson
135avm/res/network/virtual-network-gateway
  • nat-rule
Virtual Network Gateway
VNET GW
fabmas
Fabio Masciotra
136avm/res/network/virtual-wanVirtual WAN
vWAN
arnoldna
Nate Arnold
137avm/res/network/vpn-gateway
  • nat-rule
  • vpn-connection
VPN Gateway
VPN GW
fabmas
Fabio Masciotra
138avm/res/network/vpn-server-configurationVPN Server Configurationericscheffler
Eric Scheffler
139avm/res/network/vpn-siteVPN Sitefabmas
Fabio Masciotra
140avm/res/operational-insights/clusterLog Analytics Dedicated Cluster
141avm/res/operational-insights/workspace
  • data-export
  • data-source
  • linked-service
  • linked-storage-account
  • saved-search
  • storage-insight-config
  • table
Log Analytics Workspacekrbar
Kris Baranek
142avm/res/operations-management/solutionOperations Management Solutionkrbar
Kris Baranek
143avm/res/portal/dashboardPortal Dashboardkrbar
Kris Baranek
144avm/res/power-bi-dedicated/capacityPower BI Dedicated CapacityChrisSidebotham
Chris Sidebotham
145avm/res/purview/accountPurview Account
146avm/res/recovery-services/vault
  • backup-config
  • backup-fabric/protection-container/protected-item
  • backup-policy
  • replication-alert-setting
  • replication-fabric
    • replication-protection-container
      • replication-protection-container-mapping
  • replication-policy
Recovery Services Vaultalexanderojala
Alexander Ojala
147avm/res/relay/namespace
  • authorization-rule
  • hybrid-connection
    • authorization-rule
  • network-rule-set
  • wcf-relay
    • authorization-rule
Relay Namespace
148avm/res/resource-graph/queryResource Graph Querysebassem
Seif Bassem
149avm/res/resources/deployment-scriptDeployment Scriptsebassem
Seif Bassem
150avm/res/resources/resource-groupResource Group
RG
segraef
Sebastian Graef
151avm/res/search/search-service
  • shared-private-link-resource
Search Servicekrbar
Kris Baranek
152avm/res/security-insights/data-connectorSecurity Insights - Data Connector
153avm/res/security-insights/settingSecurity Insights - Setting
154avm/res/service-bus/namespace
  • authorization-rule
  • disaster-recovery-config
  • migration-configuration
  • network-rule-set
  • queue
    • authorization-rule
  • topic
    • authorization-rule
    • subscription
      • rule
Service Bus NamespaceChrisSidebotham
Chris Sidebotham
155avm/res/service-fabric/cluster
  • application-type
Service Fabric Clusterlsnoddy
Luke Snoddy
156avm/res/service-networking/traffic-controller
  • association
  • frontend
  • security-policy
Application Gateway for Containers (Traffic Controller)krbar
Kris Baranek
157avm/res/signal-r-service/signal-rSignalR Service SignalR
158avm/res/signal-r-service/web-pub-subSignalR Web PubSub Service
159avm/res/sql/instance-poolSQL Instance Poolgpacetti
Giuseppe Pacetti
160avm/res/sql/managed-instance
  • database
    • backup-long-term-retention-policy
    • backup-short-term-retention-policy
  • encryption-protector
  • key
  • security-alert-policy
  • vulnerability-assessment
SQL Managed Instance
SQL MI
chanakanissanka
Chanaka Nissanka
161avm/res/sql/server
  • auditing-setting
  • database
    • backup-long-term-retention-policy
    • backup-short-term-retention-policy
  • elastic-pool
  • encryption-protector
  • failover-group
  • firewall-rule
  • key
  • security-alert-policy
  • virtual-network-rule
  • vulnerability-assessment
Azure SQL Serverpeterbud
Peter Budai
162avm/res/storage/storage-account
Storage Accountfblix
Felix Borst
163avm/res/synapse/private-link-hubAzure Synapse Analytics Private Link HubTomazMlakar
Tomaz Mlakar
164avm/res/synapse/workspace
  • administrator
  • big-data-pool
  • firewall-rule
  • integration-runtime
  • key
  • sql-pool
Azure Synapse Analytics WorkspaceTomazMlakar
Tomaz Mlakar
165avm/res/virtual-machine-images/image-templateVirtual Machine Image Templateahmadabdalla
Ahmad Abdalla
166avm/res/web/connectionAPI Connection
167avm/res/web/hosting-environment
  • configuration-customdnssuffix
App Service Environment
ASE
tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
168avm/res/web/serverfarmApp Service Plantsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
169avm/res/web/site
  • basic-publishing-credentials-policy
  • config
  • extension
  • hybrid-connection-namespace/relay
  • slot
    • basic-publishing-credentials-policy
    • config
    • extension
    • hybrid-connection-namespace/relay
Web/Function App
App Service, Web Site, Logic App, Function App
tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
170avm/res/web/static-site
  • config
  • custom-domain
  • linked-backend
Static Web AppChrisSidebotham
Chris Sidebotham

Proposed modules - βšͺ

βž• Proposed Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/api-center/serviceAPI Center Service
02avm/res/app/agentAzure SRE AgentFallenHoot
Zach Olinske
03avm/res/avs/private-cloudAVS Private Cloudjchancellor-ms
Jon Chancellor
04avm/res/bot-service/bot-serviceBot ServiceAnkitSDesai
Ankit Desai
asishr
Asish R
05avm/res/chaos/experimentChaos Experiment
Azure Chaos Studio, Chaos Engineering
jbinko
Jiri Binko
06avm/res/code-signing/code-signing-accountAzure Artifact Signing
07avm/res/dashboard/grafanaAzure Managed Grafanavlahane
Vishal Lahane
08avm/res/data-protection/resource-guardData Protection Resource Guard
09avm/res/durable-task/schedulerDurable Task Schedulergreenie-msft
Nick Greenfield
10avm/res/edge-order/order-itemEdge Order Itemchirag1603
Chirag Choudha
11avm/res/edge/configurationEdge Configurationchirag1603
Chirag Choudha
12avm/res/edge/site
  • rg-scope
  • sub-scope
Edge Sitechirag1603
Chirag Choudha
13avm/res/hybrid-compute/private-link-scopeHybrid Compute Private Link Scopechirag1603
Chirag Choudha
14avm/res/hybrid-compute/settingHybrid Compute Settingchirag1603
Chirag Choudha
15avm/res/insights/autoscale-settingInsights - Auto Scale SettingFallenHoot
Zach Olinske
16avm/res/iot-operations/instanceIoT Operations Instanceagreaves-ms
Allen Greaves
17avm/res/key-vault/managed-hsmManaged HSM
18avm/res/kubernetes-runtime/bpg-peerKubernetes Runtime BGP Peerchirag1603
Chirag Choudha
19avm/res/kubernetes-runtime/serviceKubernetes Runtime Servicechirag1603
Chirag Choudha
20avm/res/scom/managed-instanceSCOM MI
System Center Operations Manager - Managed Instance

21avm/res/security-insights/onboarding-stateSecurity Insights - Onboarding State
22avm/res/sql-virtual-machine/sql-virtual-machineSQL Virtual Machine
SQL VM
peterbud
Peter Budai
23avm/res/stream-analytics/streaming-jobStream Analytics Job

Deprecated modules - πŸ”΄

βž• Deprecated Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/network/front-doorAzure Front Door

All modules - πŸ“‡

βž• All Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/aad/domain-serviceAzure Active Directory Domain Service
AAD, Entra ID, Microsoft Entra Domain Services, AAD DS, Azure AD DS
ReneHezser
Rene Hezser
CRYP70N1X
Paul Chirila
02avm/res/alerts-management/action-ruleAction Rulesjudyer28
Justin Dyer
03avm/res/analysis-services/serverAnalysis Services Server
04avm/res/api-center/serviceAPI Center Service
05avm/res/api-management/service
API Management Service
06avm/res/app-configuration/configuration-store
  • key-value
  • replica
App Configuration StoreJfolberth
John Folberth
07avm/res/app/agentAzure SRE AgentFallenHoot
Zach Olinske
08avm/res/app/container-app
  • auth-config
Container AppoZakari
Zach Trocinski
09avm/res/app/jobApp JobReneHezser
Rene Hezser
10avm/res/app/managed-environment
  • certificate
  • storage
App Managed Environment
11avm/res/app/session-poolApp Session Pool
12avm/res/authorization/policy-assignment
Authorization - Policy AssignmentAlexanderSehr
Alexander Sehr
13avm/res/authorization/role-assignment
Authorization - Role Assignmentarnoldna
Nate Arnold
14avm/res/automation/automation-account
  • credential
  • hybrid-runbook-worker-group
    • hybrid-runbook-worker
  • job-schedule
  • module
  • powershell72-module
  • python2-package
  • python3-package
  • runbook
  • schedule
  • source-control
  • variable
  • webhook
Automation Accountgpacetti
Giuseppe Pacetti
15avm/res/avs/private-cloudAVS Private Cloudjchancellor-ms
Jon Chancellor
16avm/res/azure-stack-hci/cluster
  • arc-setting/extension
  • deployment-setting
Azure Stack HCI Clusterchirag1603
Chirag Choudha
17avm/res/azure-stack-hci/logical-networkAzure Stack HCI Logical Networkchirag1603
Chirag Choudha
18avm/res/azure-stack-hci/marketplace-gallery-imageAzure Stack HCI Marketplace Gallery Imagechirag1603
Chirag Choudha
19avm/res/azure-stack-hci/network-interfaceAzure Stack HCI Network Interfacechirag1603
Chirag Choudha
20avm/res/azure-stack-hci/virtual-hard-diskAzure Stack HCI Hard Diskchirag1603
Chirag Choudha
21avm/res/azure-stack-hci/virtual-machine-instanceAzure Stack HCI Virtual Machine Instancechirag1603
Chirag Choudha
22avm/res/batch/batch-accountBatch Accountdidayal-msft
Divyadeep Dayal
23avm/res/bot-service/bot-serviceBot ServiceAnkitSDesai
Ankit Desai
asishr
Asish R
24avm/res/cache/redis
  • access-policy
  • access-policy-assignment
  • firewall-rule
  • linked-server
Redis CacheTomazMlakar
Tomaz Mlakar
25avm/res/cache/redis-enterprise
  • database
    • access-policy-assignment
Redis Enterprise CacheJeffreyCA
Jeffrey Chen
26avm/res/cdn/profile
CDN Profile
Azure Front Door
gbeaud
Guillaume Beaud
27avm/res/chaos/experimentChaos Experiment
Azure Chaos Studio, Chaos Engineering
jbinko
Jiri Binko
28avm/res/code-signing/code-signing-accountAzure Artifact Signing
29avm/res/cognitive-services/accountAzure AI Services (Cognitive Services)
AI Foundry
mswantek68
Mike Swantek
chanakanissanka
Chanaka Nissanka
30avm/res/communication/communication-serviceCommunication Servicedonk-msft
Don Koning
31avm/res/communication/email-service
  • domain
    • sender-username
Email Communication Servicedonk-msft
Don Koning
32avm/res/compute/availability-setAvailability Set
AS
ahmadabdalla
Ahmad Abdalla
33avm/res/compute/diskCompute Disksegraef
Sebastian Graef
34avm/res/compute/disk-encryption-setDisk Encryption Setsegraef
Sebastian Graef
35avm/res/compute/gallery
  • application
  • image
Azure Compute GalleryReneHezser
Rene Hezser
36avm/res/compute/imageImagetony-box
Tony Box
37avm/res/compute/proximity-placement-groupProximity Placement Groupjeetgarg
Jeet Garg
38avm/res/compute/ssh-public-keyPublic SSH KeyChrisSidebotham
Chris Sidebotham
39avm/res/compute/virtual-machine
  • extension
Virtual Machine
VM
josunefon
Jordi Sune Fontanals
40avm/res/compute/virtual-machine-scale-set
  • extension
Virtual Machine Scale Set
VMSS
josunefon
Jordi Sune Fontanals
41avm/res/consumption/budget
Consumption Budgetsegraef
Sebastian Graef
42avm/res/container-instance/container-groupContainer Instance
ACI
JPEasier
Julian Peißker
43avm/res/container-registry/registry
Azure Container Registry (ACR)JPEasier
Julian Peißker
44avm/res/container-service/managed-cluster
  • agent-pool
  • maintenance-configuration
Azure Kubernetes Service (AKS) Managed ClusterJPEasier
Julian Peißker
45avm/res/dashboard/grafanaAzure Managed Grafanavlahane
Vishal Lahane
46avm/res/data-factory/factory
  • integration-runtime
  • linked-service
  • managed-virtual-network
    • managed-private-endpoint
Data FactoryTomazMlakar
Tomaz Mlakar
47avm/res/data-protection/backup-vault
  • backup-instance
  • backup-policy
Data Protection Backup Vault
48avm/res/data-protection/resource-guardData Protection Resource Guard
49avm/res/databricks/access-connectorAzure Databricks Access Connector
50avm/res/databricks/workspaceAzure Databricks Workspace
51avm/res/db-for-my-sql/flexible-server
  • administrator
  • advanced-threat-protection
  • configuration
  • database
  • firewall-rule
DB for MySQL Flexible Server
52avm/res/db-for-postgre-sql/flexible-server
  • administrator
  • advanced-threat-protection-setting
  • configuration
  • database
  • firewall-rule
DB for Postgre SQL Flexible Serverarnoldna
Nate Arnold
53avm/res/desktop-virtualization/application-group
  • application
Azure Virtual Desktop (AVD) Application Group
54avm/res/desktop-virtualization/host-poolAzure Virtual Desktop (AVD) Host Pool
55avm/res/desktop-virtualization/scaling-planAzure Virtual Desktop (AVD) Scaling Plan
56avm/res/desktop-virtualization/workspaceAzure Virtual Desktop (AVD) Workspace
57avm/res/dev-center/devcenter
  • attachednetwork
  • catalog
  • devboxdefinition
  • environment-type
  • gallery
  • project-policy
Dev Centerahmadabdalla
Ahmad Abdalla
58avm/res/dev-center/network-connectionDev Center Network Connectionahmadabdalla
Ahmad Abdalla
59avm/res/dev-center/project
  • catalog
  • environment-type
  • pool
    • schedule
Dev Center Projectahmadabdalla
Ahmad Abdalla
60avm/res/dev-ops-infrastructure/poolDevOps Infrastructure Poolelizatargithub7
Eliza Tarasila
61avm/res/dev-test-lab/lab
  • artifactsource
  • cost
  • notificationchannel
  • policyset/policy
  • schedule
  • secret
  • virtualnetwork
DevTest Labahmadabdalla
Ahmad Abdalla
62avm/res/devices/iot-hubIoT HubTomazMlakar
Tomaz Mlakar
63avm/res/digital-twins/digital-twins-instance
  • endpoint
Digital Twins Instanceryanmstephens
Ryan Stephens
64avm/res/document-db/database-account
Cosmos DB Database Accountcmaneu
Christopher Maneu
65avm/res/document-db/mongo-cluster
  • firewall-rule
  • user
Cosmos DB for MongoDB (vCore)sinedied
Yohan Lasorsa
66avm/res/durable-task/schedulerDurable Task Schedulergreenie-msft
Nick Greenfield
67avm/res/edge-order/order-itemEdge Order Itemchirag1603
Chirag Choudha
68avm/res/edge/configurationEdge Configurationchirag1603
Chirag Choudha
69avm/res/edge/site
  • rg-scope
  • sub-scope
Edge Sitechirag1603
Chirag Choudha
70avm/res/elastic-san/elastic-san
  • volume-group
    • snapshot
    • volume
Elastic SAN
SAN, ESAN, Elastic SAN, Azure Elastic Storage Area Network, iSCSI, internet Small Computer Systems Interface
jbinko
Jiri Binko
71avm/res/event-grid/domain
  • event-subscription
  • topic
Event Grid Domainfabmas
Fabio Masciotra
72avm/res/event-grid/namespace
  • ca-certificate
  • client
  • client-group
  • permission-binding
  • topic
    • event-subscription
  • topic-space
Event Grid Namespacefabmas
Fabio Masciotra
73avm/res/event-grid/system-topic
  • event-subscription
Event Grid System Topicfabmas
Fabio Masciotra
74avm/res/event-grid/topic
  • event-subscription
Event Grid Topicfabmas
Fabio Masciotra
75avm/res/event-hub/namespace
  • authorization-rule
  • disaster-recovery-config
  • eventhub
    • authorization-rule
    • consumergroup
  • network-rule-set
Event Hub Namespace
76avm/res/fabric/capacityFabricmswantek68
Mike Swantek
cmaneu
Christopher Maneu
77avm/res/health-bot/health-botAzure Health Bot
78avm/res/healthcare-apis/workspace
  • dicomservice
  • fhirservice
  • iotconnector
    • fhirdestination
Healthcare API Workspace
79avm/res/hybrid-compute/gatewayHybrid Compute Gatewaychirag1603
Chirag Choudha
80avm/res/hybrid-compute/licenseHybrid Compute Licensechirag1603
Chirag Choudha
81avm/res/hybrid-compute/machine
  • extension
Hybrid Compute Machinechirag1603
Chirag Choudha
82avm/res/hybrid-compute/private-link-scopeHybrid Compute Private Link Scopechirag1603
Chirag Choudha
83avm/res/hybrid-compute/settingHybrid Compute Settingchirag1603
Chirag Choudha
84avm/res/hybrid-container-service/provisioned-cluster-instanceHybrid Container Service - Provisioned Cluster Instancechirag1603
Chirag Choudha
85avm/res/insights/action-groupAction Grouprahalan
Rainer Halanek
86avm/res/insights/activity-log-alertActivity Log Alertdonk-msft
Don Koning
87avm/res/insights/autoscale-settingInsights - Auto Scale SettingFallenHoot
Zach Olinske
88avm/res/insights/component
  • linked-storage-account
Application Insightkrbar
Kris Baranek
89avm/res/insights/data-collection-endpointData Collection Endpointkrbar
Kris Baranek
90avm/res/insights/data-collection-ruleData Collection Rule
DCR
krbar
Kris Baranek
91avm/res/insights/diagnostic-settingDiagnostic Settingkrbar
Kris Baranek
92avm/res/insights/metric-alertMetric Alertkijunkang
Ki Jun Kang
93avm/res/insights/private-link-scope
  • scoped-resource
Azure Monitor Private Link Scopeahmadabdalla
Ahmad Abdalla
94avm/res/insights/scheduled-query-ruleScheduled Query Rule
95avm/res/insights/webtestWeb TestJfolberth
John Folberth
96avm/res/iot-operations/instanceIoT Operations Instanceagreaves-ms
Allen Greaves
97avm/res/key-vault/managed-hsmManaged HSM
98avm/res/key-vault/vault
Key Vault
KV
fblix
Felix Borst
99avm/res/kubernetes-configuration/extensionKubernetes Configuration ExtensionJPEasier
Julian Peißker
100avm/res/kubernetes-configuration/flux-configurationKubernetes Configuration Flux ConfigurationJPEasier
Julian Peißker
101avm/res/kubernetes-runtime/bpg-peerKubernetes Runtime BGP Peerchirag1603
Chirag Choudha
102avm/res/kubernetes-runtime/load-balancerKubernetes Runtime Load Balancerchirag1603
Chirag Choudha
103avm/res/kubernetes-runtime/serviceKubernetes Runtime Servicechirag1603
Chirag Choudha
104avm/res/kubernetes/connected-clusterKubernetes Connected Clusterchirag1603
Chirag Choudha
105avm/res/kusto/cluster
  • database
    • principal-assignment
  • principal-assignment
Azure Data Explorer (Kusto) clusteroZakari
Zach Trocinski
106avm/res/load-test-service/load-testLoad Testing Servicesebassem
Seif Bassem
107avm/res/logic/integration-account
  • agreement
  • assembly
  • certificate
  • map
  • partner
  • schema
Logic Apps Integration Accountlsnoddy
Luke Snoddy
108avm/res/logic/workflowLogic Apps Workflowlsnoddy
Luke Snoddy
109avm/res/machine-learning-services/registryMachine Learning Services Registryjosunefon
Jordi Sune Fontanals
110avm/res/machine-learning-services/workspace
  • compute
  • connection
  • datastore
Machine Learning Services Workspace
ML Workspace
cecheta
Chinedum Echeta
ross-p-smith
Ross Smith
111avm/res/maintenance/configuration-assignmentMaintenance Configuration Assignmenteriqua
Erika Gressi
112avm/res/maintenance/maintenance-configurationMaintenance Configurationarievanderwende
Arie van der Wende
113avm/res/managed-identity/user-assigned-identity
  • federated-identity-credential
User Assigned Identity
MSI
gpacetti
Giuseppe Pacetti
114avm/res/managed-services/registration-definitionRegistration Definition (Lighthouse)
115avm/res/management/management-groupManagement Group
MG
fblix
Felix Borst
116avm/res/management/service-groupService Groupjtracey93
Jack Tracey
117avm/res/maps/accountAzure Maps Accountjhueppauff
Julian Huppauff
118avm/res/net-app/net-app-account
  • backup-policy
  • backup-vault
    • backup
  • capacity-pool
    • volume
  • snapshot-policy
Azure NetApp Filefbinotto
Felipe Binotto
119avm/res/network/application-gatewayApplication Gateway
App GW
toddbeauchemin
Todd Beauchemin
120avm/res/network/application-gateway-web-application-firewall-policyApplication Gateway Web Application Firewall (WAF) Policytoddbeauchemin
Todd Beauchemin
121avm/res/network/application-security-groupApplication Security Group (ASG)
ASG
segraef
Sebastian Graef
122avm/res/network/azure-firewallAzure Firewall
Azure FW
jtracey93
Jack Tracey
oZakari
Zach Trocinski
123avm/res/network/bastion-hostBastion Hostkrbar
Kris Baranek
124avm/res/network/connectionVirtual Network Gateway Connectionfabmas
Fabio Masciotra
125avm/res/network/ddos-protection-planDDoS Protection Plansegraef
Sebastian Graef
126avm/res/network/dns-forwarding-ruleset
  • forwarding-rule
  • virtual-network-link
DNS Forwarding RulesetChrisSidebotham
Chris Sidebotham
127avm/res/network/dns-resolver
  • inbound-endpoint
  • outbound-endpoint
DNS ResolverChrisSidebotham
Chris Sidebotham
128avm/res/network/dns-zone
  • a
  • aaaa
  • caa
  • cname
  • mx
  • ns
  • ptr
  • soa
  • srv
  • txt
Public DNS ZoneChrisSidebotham
Chris Sidebotham
129avm/res/network/express-route-circuitExpressRoute Circuit
ER Circuit
arnoldna
Nate Arnold
130avm/res/network/express-route-gatewayExpress Route Gateway
ER GW
arnoldna
Nate Arnold
131avm/res/network/express-route-portExpressRoute Port
ER Port
arnoldna
Nate Arnold
132avm/res/network/firewall-policy
  • rule-collection-group
Firewall Policyjtracey93
Jack Tracey
oZakari
Zach Trocinski
133avm/res/network/front-doorAzure Front Door
134avm/res/network/front-door-web-application-firewall-policyFront Door Web Application Firewall (WAF) PolicyPaulJohnston88
Paul Johnston
135avm/res/network/ip-groupIP Groupahmadabdalla
Ahmad Abdalla
136avm/res/network/load-balancer
  • backend-address-pool
  • inbound-nat-rule
Load Balancer
LB, NLB
arnoldna
Nate Arnold
137avm/res/network/local-network-gatewayLocal Network Gatewayfabmas
Fabio Masciotra
138avm/res/network/nat-gatewayNAT Gateway
NAT GW
fabmas
Fabio Masciotra
139avm/res/network/network-interfaceNetwork Interface
NIC
rahalan
Rainer Halanek
140avm/res/network/network-manager
  • connectivity-configuration
  • network-group
    • static-member
  • routing-configuration
    • rule-collection
      • rule
  • scope-connection
  • security-admin-configuration
    • rule-collection
      • rule
Network Managerahmadabdalla
Ahmad Abdalla
141avm/res/network/network-security-groupNetwork Security Group
NSG
ahmadabdalla
Ahmad Abdalla
142avm/res/network/network-security-perimeter
  • profile
    • access-rule
Network Security Perimeterpeterbud
Peter Budai
143avm/res/network/network-watcher
  • connection-monitor
  • flow-log
Network Watchersegraef
Sebastian Graef
144avm/res/network/p2s-vpn-gatewayP2S VPN Gatewayericscheffler
Eric Scheffler
145avm/res/network/private-dns-zone
Private DNS ZoneChrisSidebotham
Chris Sidebotham
146avm/res/network/private-endpoint
  • private-dns-zone-group
Private Endpointsegraef
Sebastian Graef
147avm/res/network/private-link-servicePrivate Link Serviceahmadabdalla
Ahmad Abdalla
148avm/res/network/public-ip-addressPublic IP Address
PIP
ChrisSidebotham
Chris Sidebotham
krbar
Kris Baranek
149avm/res/network/public-ip-prefixPublic IP Prefix
PIP Prefix
krbar
Kris Baranek
150avm/res/network/route-tableRoute Table
UDR
segraef
Sebastian Graef
151avm/res/network/service-endpoint-policyService Endpoint Policyjeetgarg
Jeet Garg
152avm/res/network/trafficmanagerprofileTraffic Manager Profilelsnoddy
Luke Snoddy
153avm/res/network/virtual-hub
  • hub-route-table
  • hub-virtual-network-connection
  • route-map
  • routing-intent
Virtual Hubarnoldna
Nate Arnold
154avm/res/network/virtual-network
Virtual Network
VNET
mjrich19
MJ Richardson
155avm/res/network/virtual-network-gateway
  • nat-rule
Virtual Network Gateway
VNET GW
fabmas
Fabio Masciotra
156avm/res/network/virtual-wanVirtual WAN
vWAN
arnoldna
Nate Arnold
157avm/res/network/vpn-gateway
  • nat-rule
  • vpn-connection
VPN Gateway
VPN GW
fabmas
Fabio Masciotra
158avm/res/network/vpn-server-configurationVPN Server Configurationericscheffler
Eric Scheffler
159avm/res/network/vpn-siteVPN Sitefabmas
Fabio Masciotra
160avm/res/operational-insights/clusterLog Analytics Dedicated Cluster
161avm/res/operational-insights/workspace
  • data-export
  • data-source
  • linked-service
  • linked-storage-account
  • saved-search
  • storage-insight-config
  • table
Log Analytics Workspacekrbar
Kris Baranek
162avm/res/operations-management/solutionOperations Management Solutionkrbar
Kris Baranek
163avm/res/portal/dashboardPortal Dashboardkrbar
Kris Baranek
164avm/res/power-bi-dedicated/capacityPower BI Dedicated CapacityChrisSidebotham
Chris Sidebotham
165avm/res/purview/accountPurview Account
166avm/res/recovery-services/vault
  • backup-config
  • backup-fabric/protection-container/protected-item
  • backup-policy
  • replication-alert-setting
  • replication-fabric
    • replication-protection-container
      • replication-protection-container-mapping
  • replication-policy
Recovery Services Vaultalexanderojala
Alexander Ojala
167avm/res/relay/namespace
  • authorization-rule
  • hybrid-connection
    • authorization-rule
  • network-rule-set
  • wcf-relay
    • authorization-rule
Relay Namespace
168avm/res/resource-graph/queryResource Graph Querysebassem
Seif Bassem
169avm/res/resources/deployment-scriptDeployment Scriptsebassem
Seif Bassem
170avm/res/resources/resource-groupResource Group
RG
segraef
Sebastian Graef
171avm/res/scom/managed-instanceSCOM MI
System Center Operations Manager - Managed Instance

172avm/res/search/search-service
  • shared-private-link-resource
Search Servicekrbar
Kris Baranek
173avm/res/security-insights/data-connectorSecurity Insights - Data Connector
174avm/res/security-insights/onboarding-stateSecurity Insights - Onboarding State
175avm/res/security-insights/settingSecurity Insights - Setting
176avm/res/service-bus/namespace
  • authorization-rule
  • disaster-recovery-config
  • migration-configuration
  • network-rule-set
  • queue
    • authorization-rule
  • topic
    • authorization-rule
    • subscription
      • rule
Service Bus NamespaceChrisSidebotham
Chris Sidebotham
177avm/res/service-fabric/cluster
  • application-type
Service Fabric Clusterlsnoddy
Luke Snoddy
178avm/res/service-networking/traffic-controller
  • association
  • frontend
  • security-policy
Application Gateway for Containers (Traffic Controller)krbar
Kris Baranek
179avm/res/signal-r-service/signal-rSignalR Service SignalR
180avm/res/signal-r-service/web-pub-subSignalR Web PubSub Service
181avm/res/sql-virtual-machine/sql-virtual-machineSQL Virtual Machine
SQL VM
peterbud
Peter Budai
182avm/res/sql/instance-poolSQL Instance Poolgpacetti
Giuseppe Pacetti
183avm/res/sql/managed-instance
  • database
    • backup-long-term-retention-policy
    • backup-short-term-retention-policy
  • encryption-protector
  • key
  • security-alert-policy
  • vulnerability-assessment
SQL Managed Instance
SQL MI
chanakanissanka
Chanaka Nissanka
184avm/res/sql/server
  • auditing-setting
  • database
    • backup-long-term-retention-policy
    • backup-short-term-retention-policy
  • elastic-pool
  • encryption-protector
  • failover-group
  • firewall-rule
  • key
  • security-alert-policy
  • virtual-network-rule
  • vulnerability-assessment
Azure SQL Serverpeterbud
Peter Budai
185avm/res/storage/storage-account
Storage Accountfblix
Felix Borst
186avm/res/stream-analytics/streaming-jobStream Analytics Job
187avm/res/synapse/private-link-hubAzure Synapse Analytics Private Link HubTomazMlakar
Tomaz Mlakar
188avm/res/synapse/workspace
  • administrator
  • big-data-pool
  • firewall-rule
  • integration-runtime
  • key
  • sql-pool
Azure Synapse Analytics WorkspaceTomazMlakar
Tomaz Mlakar
189avm/res/virtual-machine-images/image-templateVirtual Machine Image Templateahmadabdalla
Ahmad Abdalla
190avm/res/web/connectionAPI Connection
191avm/res/web/hosting-environment
  • configuration-customdnssuffix
App Service Environment
ASE
tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
192avm/res/web/serverfarmApp Service Plantsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
193avm/res/web/site
  • basic-publishing-credentials-policy
  • config
  • extension
  • hybrid-connection-namespace/relay
  • slot
    • basic-publishing-credentials-policy
    • config
    • extension
    • hybrid-connection-namespace/relay
Web/Function App
App Service, Web Site, Logic App, Function App
tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
194avm/res/web/static-site
  • config
  • custom-domain
  • linked-backend
Static Web AppChrisSidebotham
Chris Sidebotham

Module Publication History - πŸ“…

βž• Module Publication History - Module names, status and owners

Modules published in March 2026

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/cdn/profile/afd-endpoint/routeCDN Profile - AFD Endpoint Route (Child of avm/res/cdn/profile)
(Inherited): gbeaud
Guillaume Beaud
02avm/res/cdn/profile/custom-domainCDN Profile - Custom Domain (Child of avm/res/cdn/profile)
(Inherited): gbeaud
Guillaume Beaud
03avm/res/cdn/profile/origin-groupCDN Profile - Origin Group (Child of avm/res/cdn/profile)
(Inherited): gbeaud
Guillaume Beaud
04avm/res/cdn/profile/origin-group/originCDN Profile - Origin Group - Origin (Child of avm/res/cdn/profile)
(Inherited): gbeaud
Guillaume Beaud
05avm/res/cdn/profile/rule-setCDN Profile - Rule Set (Child of avm/res/cdn/profile)
(Inherited): gbeaud
Guillaume Beaud
06avm/res/cdn/profile/security-policyCDN Profile - Security Policy (Child of avm/res/cdn/profile)
(Inherited): gbeaud
Guillaume Beaud
07avm/res/container-registry/registry/cache-ruleAzure Container Registry (ACR) - Cache Rule (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
08avm/res/container-registry/registry/credential-setAzure Container Registry (ACR) - Credential Set (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
09avm/res/container-registry/registry/replicationAzure Container Registry (ACR) - Replication (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
10avm/res/container-registry/registry/scope-mapAzure Container Registry (ACR) - Scope Map (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
11avm/res/container-registry/registry/taskAzure Container Registry (ACR) - Task (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
12avm/res/container-registry/registry/tokenAzure Container Registry (ACR) - Token (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
13avm/res/container-registry/registry/webhookAzure Container Registry (ACR) - Webhook (Child of avm/res/container-registry/registry)
(Inherited): JPEasier
Julian Peißker
14avm/res/devices/iot-hubIoT HubTomazMlakar
Tomaz Mlakar
15avm/res/network/private-dns-zone/virtual-network-linkVirtual Network Links (Child of avm/res/network/private-dns-zone)
VNET Link

(Inherited): ChrisSidebotham
Chris Sidebotham
16avm/res/storage/storage-account/blob-service/container/immutability-policyStorage Account - Blob Container Immutability Policy (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst
17avm/res/storage/storage-account/local-userStorage Account - Local User (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst
18avm/res/storage/storage-account/management-policyStorage Account - Management Policy (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst
19avm/res/storage/storage-account/queue-service/queueStorage Account - Queue (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst
20avm/res/storage/storage-account/table-service/tableStorage Account - Table (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst

Modules published in January 2026

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/service-bus/namespace/queueService Bus Namespace - Queue (Child of avm/res/service-bus/namespace)
(Inherited): ChrisSidebotham
Chris Sidebotham
02avm/res/service-bus/namespace/topicService Bus Namespace - Topic (Child of avm/res/service-bus/namespace)
(Inherited): ChrisSidebotham
Chris Sidebotham

Modules published in December 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/logic/integration-accountLogic Apps Integration Accountlsnoddy
Luke Snoddy

Modules published in November 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/operational-insights/clusterLog Analytics Dedicated Cluster

Modules published in September 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/api-management/service/apiAPI Management Service - API (Child of avm/res/api-management/service)
(Inherited):
02avm/res/api-management/service/api-version-setAPI Management Service - API Version Set (Child of avm/res/api-management/service)
(Inherited):
03avm/res/api-management/service/api/diagnosticsAPI Management Service - API Diagnostics (Child of avm/res/api-management/service)
(Inherited):
04avm/res/api-management/service/api/policyAPI Management Service - API Policy (Child of avm/res/api-management/service)
(Inherited):
05avm/res/api-management/service/authorization-serverAPI Management Service - Authorization Server (Child of avm/res/api-management/service)
(Inherited):
06avm/res/api-management/service/backendAPI Management Service - Backend (Child of avm/res/api-management/service)
(Inherited):
07avm/res/api-management/service/cacheAPI Management Service - Cache (Child of avm/res/api-management/service)
(Inherited):
08avm/res/api-management/service/identity-providerAPI Management Service - Identity Provider (Child of avm/res/api-management/service)
(Inherited):
09avm/res/api-management/service/loggerAPI Management Service - Logger (Child of avm/res/api-management/service)
(Inherited):
10avm/res/api-management/service/named-valueAPI Management Service - Named Value (Child of avm/res/api-management/service)
(Inherited):
11avm/res/api-management/service/policyAPI Management Service - Policy (Child of avm/res/api-management/service)
(Inherited):
12avm/res/api-management/service/portalsettingAPI Management Service - Portal Setting (Child of avm/res/api-management/service)
(Inherited):
13avm/res/api-management/service/productAPI Management Service - Product (Child of avm/res/api-management/service)
(Inherited):
14avm/res/api-management/service/product/apiAPI Management Service - Product API (Child of avm/res/api-management/service)
(Inherited):
15avm/res/api-management/service/product/groupAPI Management Service - Product Group (Child of avm/res/api-management/service)
(Inherited):
16avm/res/api-management/service/subscriptionAPI Management Service - Subscription (Child of avm/res/api-management/service)
(Inherited):
17avm/res/authorization/policy-assignment/mg-scopeAuthorization - Policy Assignment - Management Group Scope (Child of avm/res/authorization/policy-assignment)
(Inherited): AlexanderSehr
Alexander Sehr
18avm/res/authorization/policy-assignment/rg-scopeAuthorization - Policy Assignment - Resource Group Scope (Child of avm/res/authorization/policy-assignment)
(Inherited): AlexanderSehr
Alexander Sehr
19avm/res/authorization/policy-assignment/sub-scopeAuthorization - Policy Assignment - Subscription Scope (Child of avm/res/authorization/policy-assignment)
(Inherited): AlexanderSehr
Alexander Sehr
20avm/res/consumption/budget/mg-scopeConsumption Budget - Management Group Scope (Child of avm/res/consumption/budget)
(Inherited): segraef
Sebastian Graef
21avm/res/consumption/budget/rg-scopeConsumption Budget - Resource Group Scope (Child of avm/res/consumption/budget)
(Inherited): segraef
Sebastian Graef
22avm/res/consumption/budget/sub-scopeConsumption Budget - Subscription Scope (Child of avm/res/consumption/budget)
(Inherited): segraef
Sebastian Graef
23avm/res/document-db/database-account/sql-role-assignmentCosmos DB - SQL Role Assignment (Child of avm/res/document-db/database-account)
(Inherited): cmaneu
Christopher Maneu
24avm/res/document-db/database-account/sql-role-definitionCosmos DB - SQL Role Definition (Child of avm/res/document-db/database-account)
(Inherited): cmaneu
Christopher Maneu
25avm/res/event-hub/namespace/eventhubEvent Hub (Child of avm/res/event-hub/namespace)
(Inherited):
26avm/res/kubernetes-runtime/load-balancerKubernetes Runtime Load Balancerchirag1603
Chirag Choudha
27avm/res/management/service-groupService Groupjtracey93
Jack Tracey
28avm/res/sql/server/databaseAzure SQL Database (Child of avm/res/sql/server)
(Inherited): peterbud
Peter Budai

Modules published in August 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/key-vault/vault/access-policyKey Vault - Access Policy (Child of avm/res/key-vault/vault)
(Inherited): fblix
Felix Borst
02avm/res/key-vault/vault/keyKey Vault - Key (Child of avm/res/key-vault/vault)
(Inherited): fblix
Felix Borst
03avm/res/key-vault/vault/secretKey Vault - Secret (Child of avm/res/key-vault/vault)
(Inherited): fblix
Felix Borst
04avm/res/network/private-dns-zone/aPrivate DNS Zone - A (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
05avm/res/network/private-dns-zone/aaaaPrivate DNS Zone - AAAA (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
06avm/res/network/private-dns-zone/cnamePrivate DNS Zone - CNAME (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
07avm/res/network/private-dns-zone/mxPrivate DNS Zone - MX (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
08avm/res/network/private-dns-zone/ptrPrivate DNS Zone - PTR (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
09avm/res/network/private-dns-zone/soaPrivate DNS Zone - SOA (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
10avm/res/network/private-dns-zone/srvPrivate DNS Zone - SRV (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
11avm/res/network/private-dns-zone/txtPrivate DNS Zone - TXT (Child of avm/res/network/private-dns-zone)
(Inherited): ChrisSidebotham
Chris Sidebotham
12avm/res/web/site/configWeb Site Configuration (Child of avm/res/web/site)
(Inherited): tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
13avm/res/web/site/slotWeb Site Slot (Child of avm/res/web/site)
(Inherited): tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal

Modules published in July 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/azure-stack-hci/marketplace-gallery-imageAzure Stack HCI Marketplace Gallery Imagechirag1603
Chirag Choudha
02avm/res/azure-stack-hci/virtual-machine-instanceAzure Stack HCI Virtual Machine Instancechirag1603
Chirag Choudha
03avm/res/storage/storage-account/file-service/shareStorage Account - File Share (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst

Modules published in June 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/authorization/role-assignment/mg-scopeAuthorization - Role Assignment - Management Group Scope (Child of avm/res/authorization/role-assignment)
(Inherited): arnoldna
Nate Arnold
02avm/res/authorization/role-assignment/rg-scopeAuthorization - Role Assignment - Resource Group Scope (Child of avm/res/authorization/role-assignment)
(Inherited): arnoldna
Nate Arnold
03avm/res/authorization/role-assignment/sub-scopeAuthorization - Role Assignment - Subscription Scope (Child of avm/res/authorization/role-assignment)
(Inherited): arnoldna
Nate Arnold
04avm/res/dev-center/devcenterDev Centerahmadabdalla
Ahmad Abdalla
05avm/res/dev-center/projectDev Center Projectahmadabdalla
Ahmad Abdalla
06avm/res/machine-learning-services/registryMachine Learning Services Registryjosunefon
Jordi Sune Fontanals
07avm/res/storage/storage-account/blob-service/containerStorage Account - Blob Container (Child of avm/res/storage/storage-account)
(Inherited): fblix
Felix Borst

Modules published in May 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/dev-center/network-connectionDev Center Network Connectionahmadabdalla
Ahmad Abdalla
02avm/res/security-insights/data-connectorSecurity Insights - Data Connector
03avm/res/security-insights/settingSecurity Insights - Setting

Modules published in March 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/azure-stack-hci/network-interfaceAzure Stack HCI Network Interfacechirag1603
Chirag Choudha
02avm/res/azure-stack-hci/virtual-hard-diskAzure Stack HCI Hard Diskchirag1603
Chirag Choudha
03avm/res/hybrid-container-service/provisioned-cluster-instanceHybrid Container Service - Provisioned Cluster Instancechirag1603
Chirag Choudha
04avm/res/kubernetes/connected-clusterKubernetes Connected Clusterchirag1603
Chirag Choudha
05avm/res/maintenance/configuration-assignmentMaintenance Configuration Assignmenteriqua
Erika Gressi
06avm/res/maps/accountAzure Maps Accountjhueppauff
Julian Huppauff
07avm/res/network/network-security-perimeterNetwork Security Perimeterpeterbud
Peter Budai
08avm/res/network/virtual-network/subnetVirtual Network - Subnet (Child of avm/res/network/virtual-network)
(Inherited): mjrich19
MJ Richardson

Modules published in February 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/app/session-poolApp Session Pool
02avm/res/azure-stack-hci/clusterAzure Stack HCI Clusterchirag1603
Chirag Choudha
03avm/res/azure-stack-hci/logical-networkAzure Stack HCI Logical Networkchirag1603
Chirag Choudha
04avm/res/cache/redis-enterpriseRedis Enterprise CacheJeffreyCA
Jeffrey Chen
05avm/res/hybrid-compute/gatewayHybrid Compute Gatewaychirag1603
Chirag Choudha
06avm/res/hybrid-compute/licenseHybrid Compute Licensechirag1603
Chirag Choudha

Modules published in January 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/network/express-route-portExpressRoute Port
ER Port
arnoldna
Nate Arnold

Modules published in December 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/elastic-san/elastic-sanElastic SAN
SAN, ESAN, Elastic SAN, Azure Elastic Storage Area Network, iSCSI, internet Small Computer Systems Interface
jbinko
Jiri Binko
02avm/res/network/p2s-vpn-gatewayP2S VPN Gatewayericscheffler
Eric Scheffler

Modules published in October 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/fabric/capacityFabricmswantek68
Mike Swantek
cmaneu
Christopher Maneu
02avm/res/network/vpn-server-configurationVPN Server Configurationericscheffler
Eric Scheffler
03avm/res/service-networking/traffic-controllerApplication Gateway for Containers (Traffic Controller)krbar
Kris Baranek

Modules published in September 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/dev-ops-infrastructure/poolDevOps Infrastructure Poolelizatargithub7
Eliza Tarasila

Modules published in June 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/alerts-management/action-ruleAction Rulesjudyer28
Justin Dyer
02avm/res/hybrid-compute/machineHybrid Compute Machinechirag1603
Chirag Choudha
03avm/res/kusto/clusterAzure Data Explorer (Kusto) clusteroZakari
Zach Trocinski
04avm/res/portal/dashboardPortal Dashboardkrbar
Kris Baranek

Modules published in May 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/app/jobApp JobReneHezser
Rene Hezser
02avm/res/communication/communication-serviceCommunication Servicedonk-msft
Don Koning
03avm/res/communication/email-serviceEmail Communication Servicedonk-msft
Don Koning

Modules published in April 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/aad/domain-serviceAzure Active Directory Domain Service
AAD, Entra ID, Microsoft Entra Domain Services, AAD DS, Azure AD DS
ReneHezser
Rene Hezser
CRYP70N1X
Paul Chirila
02avm/res/healthcare-apis/workspaceHealthcare API Workspace
03avm/res/load-test-service/load-testLoad Testing Servicesebassem
Seif Bassem
04avm/res/managed-services/registration-definitionRegistration Definition (Lighthouse)
05avm/res/network/application-gatewayApplication Gateway
App GW
toddbeauchemin
Todd Beauchemin
06avm/res/network/application-gateway-web-application-firewall-policyApplication Gateway Web Application Firewall (WAF) Policytoddbeauchemin
Todd Beauchemin
07avm/res/network/network-watcherNetwork Watchersegraef
Sebastian Graef
08avm/res/service-fabric/clusterService Fabric Clusterlsnoddy
Luke Snoddy
09avm/res/sql/instance-poolSQL Instance Poolgpacetti
Giuseppe Pacetti
10avm/res/sql/managed-instanceSQL Managed Instance
SQL MI
chanakanissanka
Chanaka Nissanka

Modules published in March 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/app-configuration/configuration-storeApp Configuration StoreJfolberth
John Folberth
02avm/res/cdn/profileCDN Profile
Azure Front Door
gbeaud
Guillaume Beaud
03avm/res/compute/virtual-machine-scale-setVirtual Machine Scale Set
VMSS
josunefon
Jordi Sune Fontanals
04avm/res/container-instance/container-groupContainer Instance
ACI
JPEasier
Julian Peißker
05avm/res/digital-twins/digital-twins-instanceDigital Twins Instanceryanmstephens
Ryan Stephens
06avm/res/event-grid/namespaceEvent Grid Namespacefabmas
Fabio Masciotra
07avm/res/event-hub/namespaceEvent Hub Namespace
08avm/res/network/azure-firewallAzure Firewall
Azure FW
jtracey93
Jack Tracey
oZakari
Zach Trocinski
09avm/res/network/service-endpoint-policyService Endpoint Policyjeetgarg
Jeet Garg
10avm/res/recovery-services/vaultRecovery Services Vaultalexanderojala
Alexander Ojala
11avm/res/relay/namespaceRelay Namespace
12avm/res/signal-r-service/signal-rSignalR Service SignalR
13avm/res/signal-r-service/web-pub-subSignalR Web PubSub Service
14avm/res/web/connectionAPI Connection
15avm/res/web/hosting-environmentApp Service Environment
ASE
tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal

Modules published in February 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/compute/availability-setAvailability Set
AS
ahmadabdalla
Ahmad Abdalla
02avm/res/desktop-virtualization/application-groupAzure Virtual Desktop (AVD) Application Group
03avm/res/desktop-virtualization/host-poolAzure Virtual Desktop (AVD) Host Pool
04avm/res/desktop-virtualization/scaling-planAzure Virtual Desktop (AVD) Scaling Plan
05avm/res/desktop-virtualization/workspaceAzure Virtual Desktop (AVD) Workspace
06avm/res/dev-test-lab/labDevTest Labahmadabdalla
Ahmad Abdalla
07avm/res/insights/private-link-scopeAzure Monitor Private Link Scopeahmadabdalla
Ahmad Abdalla
08avm/res/machine-learning-services/workspaceMachine Learning Services Workspace
ML Workspace
cecheta
Chinedum Echeta
ross-p-smith
Ross Smith
09avm/res/management/management-groupManagement Group
MG
fblix
Felix Borst
10avm/res/network/ip-groupIP Groupahmadabdalla
Ahmad Abdalla
11avm/res/network/network-managerNetwork Managerahmadabdalla
Ahmad Abdalla
12avm/res/network/private-link-servicePrivate Link Serviceahmadabdalla
Ahmad Abdalla
13avm/res/network/virtual-hubVirtual Hubarnoldna
Nate Arnold
14avm/res/network/virtual-wanVirtual WAN
vWAN
arnoldna
Nate Arnold
15avm/res/purview/accountPurview Account
16avm/res/synapse/private-link-hubAzure Synapse Analytics Private Link HubTomazMlakar
Tomaz Mlakar
17avm/res/synapse/workspaceAzure Synapse Analytics WorkspaceTomazMlakar
Tomaz Mlakar
18avm/res/virtual-machine-images/image-templateVirtual Machine Image Templateahmadabdalla
Ahmad Abdalla

Modules published in January 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/analysis-services/serverAnalysis Services Server
02avm/res/app/container-appContainer AppoZakari
Zach Trocinski
03avm/res/cache/redisRedis CacheTomazMlakar
Tomaz Mlakar
04avm/res/compute/diskCompute Disksegraef
Sebastian Graef
05avm/res/compute/disk-encryption-setDisk Encryption Setsegraef
Sebastian Graef
06avm/res/compute/imageImagetony-box
Tony Box
07avm/res/compute/proximity-placement-groupProximity Placement Groupjeetgarg
Jeet Garg
08avm/res/compute/virtual-machineVirtual Machine
VM
josunefon
Jordi Sune Fontanals
09avm/res/consumption/budgetConsumption Budgetsegraef
Sebastian Graef
10avm/res/container-registry/registryAzure Container Registry (ACR)JPEasier
Julian Peißker
11avm/res/container-service/managed-clusterAzure Kubernetes Service (AKS) Managed ClusterJPEasier
Julian Peißker
12avm/res/data-protection/backup-vaultData Protection Backup Vault
13avm/res/databricks/access-connectorAzure Databricks Access Connector
14avm/res/databricks/workspaceAzure Databricks Workspace
15avm/res/db-for-my-sql/flexible-serverDB for MySQL Flexible Server
16avm/res/health-bot/health-botAzure Health Bot
17avm/res/net-app/net-app-accountAzure NetApp Filefbinotto
Felipe Binotto
18avm/res/network/ddos-protection-planDDoS Protection Plansegraef
Sebastian Graef
19avm/res/network/firewall-policyFirewall Policyjtracey93
Jack Tracey
oZakari
Zach Trocinski
20avm/res/network/front-doorAzure Front Door
21avm/res/network/front-door-web-application-firewall-policyFront Door Web Application Firewall (WAF) PolicyPaulJohnston88
Paul Johnston
22avm/res/network/local-network-gatewayLocal Network Gatewayfabmas
Fabio Masciotra
23avm/res/network/nat-gatewayNAT Gateway
NAT GW
fabmas
Fabio Masciotra
24avm/res/network/virtual-network-gatewayVirtual Network Gateway
VNET GW
fabmas
Fabio Masciotra
25avm/res/network/vpn-gatewayVPN Gateway
VPN GW
fabmas
Fabio Masciotra
26avm/res/storage/storage-accountStorage Accountfblix
Felix Borst
27avm/res/web/siteWeb/Function App
App Service, Web Site, Logic App, Function App
tsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal
28avm/res/web/static-siteStatic Web AppChrisSidebotham
Chris Sidebotham

Modules published in December 2023

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/api-management/serviceAPI Management Service
02avm/res/app/managed-environmentApp Managed Environment
03avm/res/automation/automation-accountAutomation Accountgpacetti
Giuseppe Pacetti
04avm/res/compute/galleryAzure Compute GalleryReneHezser
Rene Hezser
05avm/res/data-factory/factoryData FactoryTomazMlakar
Tomaz Mlakar
06avm/res/document-db/database-accountCosmos DB Database Accountcmaneu
Christopher Maneu
07avm/res/insights/activity-log-alertActivity Log Alertdonk-msft
Don Koning
08avm/res/insights/data-collection-endpointData Collection Endpointkrbar
Kris Baranek
09avm/res/insights/data-collection-ruleData Collection Rule
DCR
krbar
Kris Baranek
10avm/res/insights/metric-alertMetric Alertkijunkang
Ki Jun Kang
11avm/res/insights/scheduled-query-ruleScheduled Query Rule
12avm/res/insights/webtestWeb TestJfolberth
John Folberth
13avm/res/maintenance/maintenance-configurationMaintenance Configurationarievanderwende
Arie van der Wende
14avm/res/managed-identity/user-assigned-identityUser Assigned Identity
MSI
gpacetti
Giuseppe Pacetti
15avm/res/network/application-security-groupApplication Security Group (ASG)
ASG
segraef
Sebastian Graef
16avm/res/network/bastion-hostBastion Hostkrbar
Kris Baranek
17avm/res/network/connectionVirtual Network Gateway Connectionfabmas
Fabio Masciotra
18avm/res/network/network-security-groupNetwork Security Group
NSG
ahmadabdalla
Ahmad Abdalla
19avm/res/network/public-ip-prefixPublic IP Prefix
PIP Prefix
krbar
Kris Baranek
20avm/res/network/trafficmanagerprofileTraffic Manager Profilelsnoddy
Luke Snoddy
21avm/res/network/vpn-siteVPN Sitefabmas
Fabio Masciotra
22avm/res/resources/resource-groupResource Group
RG
segraef
Sebastian Graef
23avm/res/service-bus/namespaceService Bus NamespaceChrisSidebotham
Chris Sidebotham
24avm/res/web/serverfarmApp Service Plantsc-buddy
Buddy Davies
pankajagrawal16
Pankaj Agrawal

Modules published in November 2023

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/batch/batch-accountBatch Accountdidayal-msft
Divyadeep Dayal
02avm/res/db-for-postgre-sql/flexible-serverDB for Postgre SQL Flexible Serverarnoldna
Nate Arnold
03avm/res/event-grid/domainEvent Grid Domainfabmas
Fabio Masciotra
04avm/res/event-grid/system-topicEvent Grid System Topicfabmas
Fabio Masciotra
05avm/res/event-grid/topicEvent Grid Topicfabmas
Fabio Masciotra
06avm/res/insights/componentApplication Insightkrbar
Kris Baranek
07avm/res/insights/diagnostic-settingDiagnostic Settingkrbar
Kris Baranek
08avm/res/logic/workflowLogic Apps Workflowlsnoddy
Luke Snoddy
09avm/res/network/express-route-circuitExpressRoute Circuit
ER Circuit
arnoldna
Nate Arnold
10avm/res/network/express-route-gatewayExpress Route Gateway
ER GW
arnoldna
Nate Arnold
11avm/res/network/load-balancerLoad Balancer
LB, NLB
arnoldna
Nate Arnold
12avm/res/network/route-tableRoute Table
UDR
segraef
Sebastian Graef
13avm/res/network/virtual-networkVirtual Network
VNET
mjrich19
MJ Richardson
14avm/res/operational-insights/workspaceLog Analytics Workspacekrbar
Kris Baranek
15avm/res/operations-management/solutionOperations Management Solutionkrbar
Kris Baranek
16avm/res/power-bi-dedicated/capacityPower BI Dedicated CapacityChrisSidebotham
Chris Sidebotham
17avm/res/resource-graph/queryResource Graph Querysebassem
Seif Bassem
18avm/res/resources/deployment-scriptDeployment Scriptsebassem
Seif Bassem
19avm/res/search/search-serviceSearch Servicekrbar
Kris Baranek
20avm/res/sql/serverAzure SQL Serverpeterbud
Peter Budai

Modules published in October 2023

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/res/cognitive-services/accountAzure AI Services (Cognitive Services)
AI Foundry
mswantek68
Mike Swantek
chanakanissanka
Chanaka Nissanka
02avm/res/compute/ssh-public-keyPublic SSH KeyChrisSidebotham
Chris Sidebotham
03avm/res/document-db/mongo-clusterCosmos DB for MongoDB (vCore)sinedied
Yohan Lasorsa
04avm/res/insights/action-groupAction Grouprahalan
Rainer Halanek
05avm/res/key-vault/vaultKey Vault
KV
fblix
Felix Borst
06avm/res/kubernetes-configuration/extensionKubernetes Configuration ExtensionJPEasier
Julian Peißker
07avm/res/kubernetes-configuration/flux-configurationKubernetes Configuration Flux ConfigurationJPEasier
Julian Peißker
08avm/res/network/dns-forwarding-rulesetDNS Forwarding RulesetChrisSidebotham
Chris Sidebotham
09avm/res/network/dns-resolverDNS ResolverChrisSidebotham
Chris Sidebotham
10avm/res/network/dns-zonePublic DNS ZoneChrisSidebotham
Chris Sidebotham
11avm/res/network/network-interfaceNetwork Interface
NIC
rahalan
Rainer Halanek
12avm/res/network/private-dns-zonePrivate DNS ZoneChrisSidebotham
Chris Sidebotham
13avm/res/network/private-endpointPrivate Endpointsegraef
Sebastian Graef
14avm/res/network/public-ip-addressPublic IP Address
PIP
ChrisSidebotham
Chris Sidebotham
krbar
Kris Baranek

Consistent Features & Extension Resources (Interfaces)

βž• Consistent Features & Extension Resources (Interfaces)

The following table shows which Bicep resource modules have which consistent features and extension resources (interfaces) implemented as defined in the Bicep Interfaces specification.

#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
1avm/res/aad/domain-serviceβœ…βœ…βœ…βœ…
2avm/res/alerts-management/action-ruleβœ…βœ…βœ…
3avm/res/analysis-services/serverβœ…βœ…βœ…βœ…
4avm/res/api-management/serviceβœ…βœ…βœ…βœ…βœ…βœ…
5avm/res/app-configuration/configuration-storeβœ…βœ…βœ…βœ…βœ…βœ…βœ…
6avm/res/app/container-appβœ…βœ…βœ…βœ…βœ…
7avm/res/app/jobβœ…βœ…βœ…βœ…
8avm/res/app/managed-environmentβœ…βœ…βœ…βœ…
9avm/res/app/session-poolβœ…βœ…βœ…βœ…
10avm/res/authorization/policy-assignment
11avm/res/authorization/role-assignment
12avm/res/automation/automation-accountβœ…βœ…βœ…βœ…βœ…βœ…βœ…
13avm/res/azure-stack-hci/clusterβœ…βœ…
14avm/res/azure-stack-hci/logical-networkβœ…βœ…
15avm/res/azure-stack-hci/marketplace-gallery-imageβœ…βœ…
16avm/res/azure-stack-hci/network-interfaceβœ…βœ…
17avm/res/azure-stack-hci/virtual-hard-diskβœ…βœ…
18avm/res/azure-stack-hci/virtual-machine-instanceβœ…
19avm/res/batch/batch-accountβœ…βœ…βœ…βœ…βœ…βœ…βœ…
20avm/res/cache/redisβœ…βœ…βœ…βœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
21avm/res/cache/redis-enterpriseβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
22avm/res/cdn/profileβœ…βœ…βœ…βœ…βœ…
23avm/res/cognitive-services/accountβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
24avm/res/communication/communication-serviceβœ…βœ…βœ…βœ…βœ…
25avm/res/communication/email-serviceβœ…βœ…βœ…
26avm/res/compute/availability-setβœ…βœ…βœ…
27avm/res/compute/diskβœ…βœ…βœ…
28avm/res/compute/disk-encryption-setβœ…βœ…βœ…βœ…βœ…βœ…
29avm/res/compute/galleryβœ…βœ…βœ…
30avm/res/compute/imageβœ…βœ…
31avm/res/compute/proximity-placement-groupβœ…βœ…βœ…
32avm/res/compute/ssh-public-keyβœ…βœ…βœ…
33avm/res/compute/virtual-machineβœ…βœ…βœ…βœ…
34avm/res/compute/virtual-machine-scale-setβœ…βœ…βœ…βœ…βœ…
35avm/res/consumption/budget
36avm/res/container-instance/container-groupβœ…βœ…βœ…βœ…
37avm/res/container-registry/registryβœ…βœ…βœ…βœ…βœ…βœ…βœ…
38avm/res/container-service/managed-clusterβœ…βœ…βœ…βœ…βœ…
39avm/res/data-factory/factoryβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
40avm/res/data-protection/backup-vaultβœ…βœ…βœ…βœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
41avm/res/databricks/access-connectorβœ…βœ…βœ…βœ…
42avm/res/databricks/workspaceβœ…βœ…βœ…βœ…βœ…βœ…βœ…
43avm/res/db-for-my-sql/flexible-serverβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
44avm/res/db-for-postgre-sql/flexible-serverβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
45avm/res/desktop-virtualization/application-groupβœ…βœ…βœ…βœ…
46avm/res/desktop-virtualization/host-poolβœ…βœ…βœ…βœ…βœ…
47avm/res/desktop-virtualization/scaling-planβœ…βœ…βœ…βœ…
48avm/res/desktop-virtualization/workspaceβœ…βœ…βœ…βœ…βœ…
49avm/res/dev-center/devcenterβœ…βœ…βœ…βœ…βœ…
50avm/res/dev-center/network-connectionβœ…βœ…βœ…
51avm/res/dev-center/projectβœ…βœ…βœ…βœ…
52avm/res/dev-ops-infrastructure/poolβœ…βœ…βœ…βœ…βœ…
53avm/res/dev-test-lab/labβœ…βœ…βœ…βœ…
54avm/res/devices/iot-hubβœ…βœ…βœ…βœ…βœ…βœ…
55avm/res/digital-twins/digital-twins-instanceβœ…βœ…βœ…βœ…βœ…βœ…
56avm/res/document-db/database-accountβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
57avm/res/document-db/mongo-clusterβœ…βœ…βœ…βœ…βœ…
58avm/res/elastic-san/elastic-sanβœ…βœ…βœ…βœ…
59avm/res/event-grid/domainβœ…βœ…βœ…βœ…βœ…βœ…
60avm/res/event-grid/namespaceβœ…βœ…βœ…βœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
61avm/res/event-grid/system-topicβœ…βœ…βœ…βœ…βœ…
62avm/res/event-grid/topicβœ…βœ…βœ…βœ…βœ…βœ…
63avm/res/event-hub/namespaceβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
64avm/res/fabric/capacityβœ…βœ…
65avm/res/health-bot/health-botβœ…βœ…βœ…βœ…
66avm/res/healthcare-apis/workspaceβœ…βœ…βœ…
67avm/res/hybrid-compute/gatewayβœ…βœ…
68avm/res/hybrid-compute/licenseβœ…
69avm/res/hybrid-compute/machineβœ…βœ…βœ…
70avm/res/hybrid-container-service/provisioned-cluster-instanceβœ…
71avm/res/insights/action-groupβœ…βœ…βœ…
72avm/res/insights/activity-log-alertβœ…βœ…βœ…
73avm/res/insights/componentβœ…βœ…βœ…βœ…
74avm/res/insights/data-collection-endpointβœ…βœ…βœ…
75avm/res/insights/data-collection-ruleβœ…βœ…βœ…βœ…
76avm/res/insights/diagnostic-setting
77avm/res/insights/metric-alertβœ…βœ…βœ…
78avm/res/insights/private-link-scopeβœ…βœ…βœ…βœ…
79avm/res/insights/scheduled-query-ruleβœ…βœ…βœ…βœ…
80avm/res/insights/webtestβœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
81avm/res/key-vault/vaultβœ…βœ…βœ…βœ…βœ…
82avm/res/kubernetes-configuration/extension
83avm/res/kubernetes-configuration/flux-configuration
84avm/res/kubernetes-runtime/load-balancer
85avm/res/kubernetes/connected-clusterβœ…βœ…
86avm/res/kusto/clusterβœ…βœ…βœ…βœ…βœ…βœ…βœ…
87avm/res/load-test-service/load-testβœ…βœ…βœ…βœ…βœ…
88avm/res/logic/integration-accountβœ…βœ…βœ…βœ…
89avm/res/logic/workflowβœ…βœ…βœ…βœ…βœ…
90avm/res/machine-learning-services/registryβœ…βœ…βœ…βœ…βœ…
91avm/res/machine-learning-services/workspaceβœ…βœ…βœ…βœ…βœ…βœ…βœ…
92avm/res/maintenance/configuration-assignment
93avm/res/maintenance/maintenance-configurationβœ…βœ…βœ…
94avm/res/managed-identity/user-assigned-identityβœ…βœ…βœ…
95avm/res/managed-services/registration-definition
96avm/res/management/management-group
97avm/res/management/service-groupβœ…βœ…
98avm/res/maps/accountβœ…βœ…βœ…βœ…βœ…
99avm/res/net-app/net-app-accountβœ…βœ…βœ…βœ…βœ…βœ…
100avm/res/network/application-gatewayβœ…βœ…βœ…βœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
101avm/res/network/application-gateway-web-application-firewall-policyβœ…βœ…βœ…
102avm/res/network/application-security-groupβœ…βœ…βœ…
103avm/res/network/azure-firewallβœ…βœ…βœ…βœ…
104avm/res/network/bastion-hostβœ…βœ…βœ…βœ…
105avm/res/network/connectionβœ…βœ…
106avm/res/network/ddos-protection-planβœ…βœ…βœ…
107avm/res/network/dns-forwarding-rulesetβœ…βœ…βœ…
108avm/res/network/dns-resolverβœ…βœ…βœ…
109avm/res/network/dns-zoneβœ…βœ…βœ…
110avm/res/network/express-route-circuitβœ…βœ…βœ…βœ…
111avm/res/network/express-route-gatewayβœ…βœ…βœ…
112avm/res/network/express-route-portβœ…βœ…βœ…βœ…
113avm/res/network/firewall-policyβœ…βœ…βœ…βœ…
114avm/res/network/front-doorβœ…βœ…βœ…βœ…
115avm/res/network/front-door-web-application-firewall-policyβœ…βœ…βœ…
116avm/res/network/ip-groupβœ…βœ…βœ…
117avm/res/network/load-balancerβœ…βœ…βœ…βœ…
118avm/res/network/local-network-gatewayβœ…βœ…βœ…
119avm/res/network/nat-gatewayβœ…βœ…βœ…
120avm/res/network/network-interfaceβœ…βœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
121avm/res/network/network-managerβœ…βœ…βœ…
122avm/res/network/network-security-groupβœ…βœ…βœ…βœ…
123avm/res/network/network-security-perimeterβœ…βœ…βœ…βœ…
124avm/res/network/network-watcherβœ…βœ…βœ…
125avm/res/network/p2s-vpn-gatewayβœ…βœ…
126avm/res/network/private-dns-zoneβœ…βœ…βœ…
127avm/res/network/private-endpointβœ…βœ…βœ…
128avm/res/network/private-link-serviceβœ…βœ…βœ…
129avm/res/network/public-ip-addressβœ…βœ…βœ…βœ…
130avm/res/network/public-ip-prefixβœ…βœ…βœ…
131avm/res/network/route-tableβœ…βœ…βœ…
132avm/res/network/service-endpoint-policyβœ…βœ…βœ…
133avm/res/network/trafficmanagerprofileβœ…βœ…βœ…βœ…
134avm/res/network/virtual-hubβœ…βœ…
135avm/res/network/virtual-networkβœ…βœ…βœ…βœ…
136avm/res/network/virtual-network-gatewayβœ…βœ…βœ…βœ…
137avm/res/network/virtual-wanβœ…βœ…βœ…
138avm/res/network/vpn-gatewayβœ…βœ…
139avm/res/network/vpn-server-configurationβœ…βœ…
140avm/res/network/vpn-siteβœ…βœ…βœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
141avm/res/operational-insights/clusterβœ…βœ…βœ…βœ…βœ…βœ…
142avm/res/operational-insights/workspaceβœ…βœ…βœ…βœ…βœ…
143avm/res/operations-management/solution
144avm/res/portal/dashboardβœ…βœ…βœ…
145avm/res/power-bi-dedicated/capacityβœ…βœ…βœ…
146avm/res/purview/accountβœ…βœ…βœ…βœ…βœ…
147avm/res/recovery-services/vaultβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
148avm/res/relay/namespaceβœ…βœ…βœ…βœ…βœ…
149avm/res/resource-graph/queryβœ…βœ…βœ…
150avm/res/resources/deployment-scriptβœ…βœ…βœ…βœ…
151avm/res/resources/resource-groupβœ…βœ…βœ…
152avm/res/search/search-serviceβœ…βœ…βœ…βœ…βœ…βœ…
153avm/res/security-insights/data-connector
154avm/res/security-insights/settingβœ…
155avm/res/service-bus/namespaceβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
156avm/res/service-fabric/clusterβœ…βœ…βœ…
157avm/res/service-networking/traffic-controllerβœ…βœ…βœ…βœ…
158avm/res/signal-r-service/signal-rβœ…βœ…βœ…βœ…βœ…βœ…
159avm/res/signal-r-service/web-pub-subβœ…βœ…βœ…βœ…βœ…
160avm/res/sql/instance-poolβœ…
#ModuleRBACLocksTagsDiagPECMKCMK-mHSMIdentity
161avm/res/sql/managed-instanceβœ…βœ…βœ…βœ…βœ…
162avm/res/sql/serverβœ…βœ…βœ…βœ…βœ…βœ…βœ…
163avm/res/storage/storage-accountβœ…βœ…βœ…βœ…βœ…βœ…βœ…βœ…
164avm/res/synapse/private-link-hubβœ…βœ…βœ…βœ…
165avm/res/synapse/workspaceβœ…βœ…βœ…βœ…βœ…βœ…βœ…
166avm/res/virtual-machine-images/image-templateβœ…βœ…βœ…βœ…
167avm/res/web/connectionβœ…βœ…βœ…
168avm/res/web/hosting-environmentβœ…βœ…βœ…βœ…βœ…
169avm/res/web/serverfarmβœ…βœ…βœ…βœ…βœ…
170avm/res/web/siteβœ…βœ…βœ…βœ…βœ…βœ…
171avm/res/web/static-siteβœ…βœ…βœ…βœ…βœ…
Sum1481461566840261767

For Module Owners & Contributors

Note

This section is mainly intended for module owners and contributors as it contains information important for module development, such as telemetry ID prefix, and GitHub Teams for Owners.

Module name, Telemetry ID prefix, GitHub Teams for Owners

βž• All Modules - Module name, Telemetry ID prefix, GitHub Teams for Owners
No.Module NameTelemetry ID prefixGitHub Teams for Module Owners (@Azure org)
01avm/res/aad/domain-service46d3xbcp.res.aad-domainserviceavm-res-aad-domainservice-module-owners-bicep
02avm/res/alerts-management/action-rule46d3xbcp.res.alertsmanagement-actionruleavm-res-alertsmanagement-actionrule-module-owners-bicep
03avm/res/analysis-services/server46d3xbcp.res.analysisservices-serveravm-res-analysisservices-server-module-owners-bicep
04avm/res/api-center/service46d3xbcp.res.apicenter-serviceavm-res-apicenter-service-module-owners-bicep
05avm/res/api-management/service46d3xbcp.res.apimanagement-serviceavm-res-apimanagement-service-module-owners-bicep
06avm/res/api-management/service/api46d3xbcp.res.apimgmt-apiavm-res-apimanagement-service-module-owners-bicep
07avm/res/api-management/service/api-version-set46d3xbcp.res.apimgmt-apiversionsetavm-res-apimanagement-service-module-owners-bicep
08avm/res/api-management/service/api/diagnostics46d3xbcp.res.apimgm-apidiagnosticsavm-res-apimanagement-service-module-owners-bicep
09avm/res/api-management/service/api/operation46d3xbcp.res.apimgmt-service-apioperationavm-res-apimanagement-service-module-owners-bicep
10avm/res/api-management/service/api/operation/policy46d3xbcp.res.apimgmt-service-apioperationpolicyavm-res-apimanagement-service-module-owners-bicep
11avm/res/api-management/service/api/policy46d3xbcp.res.apimgmt-apipolicyavm-res-apimanagement-service-module-owners-bicep
12avm/res/api-management/service/authorization-server46d3xbcp.res.apimgmt-authzserveravm-res-apimanagement-service-module-owners-bicep
13avm/res/api-management/service/backend46d3xbcp.res.apimgmt-backendavm-res-apimanagement-service-module-owners-bicep
14avm/res/api-management/service/cache46d3xbcp.res.apimgmt-cacheavm-res-apimanagement-service-module-owners-bicep
15avm/res/api-management/service/diagnostics46d3xbcp.res.apimgmt-service-diagavm-res-apimanagement-service-module-owners-bicep
16avm/res/api-management/service/identity-provider46d3xbcp.res.apimgmt-identityprovideravm-res-apimanagement-service-module-owners-bicep
17avm/res/api-management/service/logger46d3xbcp.res.apimgmt-loggeravm-res-apimanagement-service-module-owners-bicep
18avm/res/api-management/service/named-value46d3xbcp.res.apimgmt-namedvalueavm-res-apimanagement-service-module-owners-bicep
19avm/res/api-management/service/policy46d3xbcp.res.apimgmt-policyavm-res-apimanagement-service-module-owners-bicep
20avm/res/api-management/service/portalsetting46d3xbcp.res.apimgmt-portalsettingavm-res-apimanagement-service-module-owners-bicep
21avm/res/api-management/service/private-endpoint-connection46d3xbcp.res.apimgmt-privendpointconnavm-res-apimanagement-service-module-owners-bicep
22avm/res/api-management/service/product46d3xbcp.res.apimgmt-productavm-res-apimanagement-service-module-owners-bicep
23avm/res/api-management/service/product/api46d3xbcp.res.apimgmt-productapiavm-res-apimanagement-service-module-owners-bicep
24avm/res/api-management/service/product/group46d3xbcp.res.apimgmt-productgroupavm-res-apimanagement-service-module-owners-bicep
25avm/res/api-management/service/product/policy46d3xbcp.res.apimgmt-service-productpolicyavm-res-apimanagement-service-module-owners-bicep
26avm/res/api-management/service/subscription46d3xbcp.res.apimgmt-subscriptionavm-res-apimanagement-service-module-owners-bicep
27avm/res/api-management/service/workspace46d3xbcp.res.apimgmt-workspaceavm-res-apimanagement-service-module-owners-bicep
28avm/res/api-management/service/workspace/api46d3xbcp.res.apimgmt-service-workspaceapiavm-res-apimanagement-service-module-owners-bicep
29avm/res/api-management/service/workspace/api-version-set46d3xbcp.res.apimgmt-service-workspaceapiversetavm-res-apimanagement-service-module-owners-bicep
30avm/res/api-management/service/workspace/api/diagnostics46d3xbcp.res.apimgmt-service-workspaceapidiagavm-res-apimanagement-service-module-owners-bicep
31avm/res/api-management/service/workspace/api/operation46d3xbcp.res.apimgmt-service-workspaceapiopavm-res-apimanagement-service-module-owners-bicep
32avm/res/api-management/service/workspace/api/operation/policy46d3xbcp.res.apimgmt-service-workspaceapioppolavm-res-apimanagement-service-module-owners-bicep
33avm/res/api-management/service/workspace/api/policy46d3xbcp.res.apimgmt-service-workspaceapipolavm-res-apimanagement-service-module-owners-bicep
34avm/res/api-management/service/workspace/backend46d3xbcp.res.apimgmt-service-workspacebackendavm-res-apimanagement-service-module-owners-bicep
35avm/res/api-management/service/workspace/diagnostics46d3xbcp.res.apimgmt-service-workspacediagavm-res-apimanagement-service-module-owners-bicep
36avm/res/api-management/service/workspace/logger46d3xbcp.res.apimgmt-service-workspaceloggeravm-res-apimanagement-service-module-owners-bicep
37avm/res/api-management/service/workspace/named-value46d3xbcp.res.apimgmt-service-workspacenamedvalavm-res-apimanagement-service-module-owners-bicep
38avm/res/api-management/service/workspace/policy46d3xbcp.res.apimgmt-service-workspacepolicyavm-res-apimanagement-service-module-owners-bicep
39avm/res/api-management/service/workspace/product46d3xbcp.res.apimgmt-service-workspaceproductavm-res-apimanagement-service-module-owners-bicep
40avm/res/api-management/service/workspace/product/api-link46d3xbcp.res.apimgmt-service-workspprodapilinkavm-res-apimanagement-service-module-owners-bicep
41avm/res/api-management/service/workspace/product/group-link46d3xbcp.res.apimgmt-service-workspprodgrplinkavm-res-apimanagement-service-module-owners-bicep
42avm/res/api-management/service/workspace/product/policy46d3xbcp.res.apimgmt-service-workspprodpolicyavm-res-apimanagement-service-module-owners-bicep
43avm/res/api-management/service/workspace/subscription46d3xbcp.res.apimgmt-service-workspacesubavm-res-apimanagement-service-module-owners-bicep
44avm/res/app-configuration/configuration-store46d3xbcp.res.appconfiguration-configurationstoreavm-res-appconfiguration-configurationstore-module-owners-bicep
45avm/res/app-configuration/configuration-store/key-value46d3xbcp.res.appconfig-configstore-keyvalueavm-res-appconfiguration-configurationstore-module-owners-bicep
46avm/res/app-configuration/configuration-store/replica46d3xbcp.res.appconfig-configstore-replicaavm-res-appconfiguration-configurationstore-module-owners-bicep
47avm/res/app/agent46d3xbcp.res.app-agentavm-res-app-agent-module-owners-bicep
48avm/res/app/container-app46d3xbcp.res.app-containerappavm-res-app-containerapp-module-owners-bicep
49avm/res/app/container-app/auth-config46d3xbcp.res.app-containerapp-authconfigavm-res-app-containerapp-module-owners-bicep
50avm/res/app/job46d3xbcp.res.app-jobavm-res-app-job-module-owners-bicep
51avm/res/app/managed-environment46d3xbcp.res.app-managedenvironmentavm-res-app-managedenvironment-module-owners-bicep
52avm/res/app/managed-environment/certificate46d3xbcp.res.app-managedenvironment-certificateavm-res-app-managedenvironment-module-owners-bicep
53avm/res/app/managed-environment/storage46d3xbcp.res.app-managedenvironment-storageavm-res-app-managedenvironment-module-owners-bicep
54avm/res/app/session-pool46d3xbcp.res.app-sessionpoolavm-res-app-sessionpool-module-owners-bicep
55avm/res/authorization/policy-assignment46d3xbcp.res.authz-policyassignmentavm-res-authorization-policyassignment-module-owners-bicep
56avm/res/authorization/policy-assignment/mg-scope46d3xbcp.res.authz-policyassignment_mgscopeavm-res-authorization-policyassignment-module-owners-bicep
57avm/res/authorization/policy-assignment/rg-scope46d3xbcp.res.authz-policyassignment_rgscopeavm-res-authorization-policyassignment-module-owners-bicep
58avm/res/authorization/policy-assignment/sub-scope46d3xbcp.res.authz-policyassignment_subscopeavm-res-authorization-policyassignment-module-owners-bicep
59avm/res/authorization/role-assignment46d3xbcp.res.authz-roleassignmentavm-res-authorization-roleassignment-module-owners-bicep
60avm/res/authorization/role-assignment/mg-scope46d3xbcp.res.authz-roleassignment_mgscopeavm-res-authorization-roleassignment-module-owners-bicep
61avm/res/authorization/role-assignment/rg-scope46d3xbcp.res.authz-roleassignment_rgscopeavm-res-authorization-roleassignment-module-owners-bicep
62avm/res/authorization/role-assignment/sub-scope46d3xbcp.res.authz-roleassignment_subscopeavm-res-authorization-roleassignment-module-owners-bicep
63avm/res/automation/automation-account46d3xbcp.res.automation-automationaccountavm-res-automation-automationaccount-module-owners-bicep
64avm/res/automation/automation-account/credential46d3xbcp.res.aut-autacct-credentialavm-res-automation-automationaccount-module-owners-bicep
65avm/res/automation/automation-account/hybrid-runbook-worker-group46d3xbcp.res.aut-autacct-hybrunbookwrkrgrpavm-res-automation-automationaccount-module-owners-bicep
66avm/res/automation/automation-account/hybrid-runbook-worker-group/hybrid-runbook-worker46d3xbcp.res.aut-autacct-hybrunbkwrkrgrphybruboavm-res-automation-automationaccount-module-owners-bicep
67avm/res/automation/automation-account/job-schedule46d3xbcp.res.aut-autacct-jobscheduleavm-res-automation-automationaccount-module-owners-bicep
68avm/res/automation/automation-account/module46d3xbcp.res.aut-autacct-moduleavm-res-automation-automationaccount-module-owners-bicep
69avm/res/automation/automation-account/powershell72-module46d3xbcp.res.aut-autacct-powersh72moduleavm-res-automation-automationaccount-module-owners-bicep
70avm/res/automation/automation-account/python2-package46d3xbcp.res.aut-autacct-python2packageavm-res-automation-automationaccount-module-owners-bicep
71avm/res/automation/automation-account/python3-package46d3xbcp.res.aut-autacct-python3packageavm-res-automation-automationaccount-module-owners-bicep
72avm/res/automation/automation-account/runbook46d3xbcp.res.aut-autacct-runbookavm-res-automation-automationaccount-module-owners-bicep
73avm/res/automation/automation-account/schedule46d3xbcp.res.aut-autacct-scheduleavm-res-automation-automationaccount-module-owners-bicep
74avm/res/automation/automation-account/source-control46d3xbcp.res.aut-autacct-sourcecontrolavm-res-automation-automationaccount-module-owners-bicep
75avm/res/automation/automation-account/variable46d3xbcp.res.aut-autacct-variableavm-res-automation-automationaccount-module-owners-bicep
76avm/res/automation/automation-account/webhook46d3xbcp.res.aut-autacct-webhookavm-res-automation-automationaccount-module-owners-bicep
77avm/res/avs/private-cloud46d3xbcp.res.avs-privatecloudavm-res-avs-privatecloud-module-owners-bicep
78avm/res/azure-stack-hci/cluster46d3xbcp.res.azurestackhci-clusteravm-res-azurestackhci-cluster-module-owners-bicep
79avm/res/azure-stack-hci/cluster/arc-setting/extension46d3xbcp.res.azurestackhci-clusterarcsettingavm-res-azurestackhci-cluster-module-owners-bicep
80avm/res/azure-stack-hci/cluster/deployment-setting46d3xbcp.res.azurestackhci-cluster-deplsettingavm-res-azurestackhci-cluster-module-owners-bicep
81avm/res/azure-stack-hci/logical-network46d3xbcp.res.azurestackhci-logicalnetworkavm-res-azurestackhci-logicalnetwork-module-owners-bicep
82avm/res/azure-stack-hci/marketplace-gallery-image46d3xbcp.res.azurestackhci-markplgalleryimgavm-res-azurestackhci-marketplacegalleryimage-module-owners-bicep
83avm/res/azure-stack-hci/network-interface46d3xbcp.res.azurestackhci-networkinterfaceavm-res-azurestackhci-networkinterface-module-owners-bicep
84avm/res/azure-stack-hci/virtual-hard-disk46d3xbcp.res.azurestackhci-virtualharddiskavm-res-azurestackhci-virtualharddisk-module-owners-bicep
85avm/res/azure-stack-hci/virtual-machine-instance46d3xbcp.res.azurestackhci-virtualmachineinstanceavm-res-azurestackhci-virtualmachineinstance-module-owners-bicep
86avm/res/batch/batch-account46d3xbcp.res.batch-batchaccountavm-res-batch-batchaccount-module-owners-bicep
87avm/res/bot-service/bot-service46d3xbcp.res.botservice-botserviceavm-res-botservice-botservice-module-owners-bicep
88avm/res/cache/redis46d3xbcp.res.cache-redisavm-res-cache-redis-module-owners-bicep
89avm/res/cache/redis-enterprise46d3xbcp.res.cache-redisenterpriseavm-res-cache-redisenterprise-module-owners-bicep
90avm/res/cache/redis-enterprise/database46d3xbcp.res.cache-redisenterprise-databaseavm-res-cache-redisenterprise-module-owners-bicep
91avm/res/cache/redis-enterprise/database/access-policy-assignment46d3xbcp.res.cache-redisenterprise-databaseaccessavm-res-cache-redisenterprise-module-owners-bicep
92avm/res/cache/redis/access-policy46d3xbcp.res.cache-redis-accesspolicyavm-res-cache-redis-module-owners-bicep
93avm/res/cache/redis/access-policy-assignment46d3xbcp.res.cache-redis-accesspolicyassignmentavm-res-cache-redis-module-owners-bicep
94avm/res/cache/redis/firewall-rule46d3xbcp.res.cache-redis-firewallruleavm-res-cache-redis-module-owners-bicep
95avm/res/cache/redis/linked-server46d3xbcp.res.cache-redis-linkedserveravm-res-cache-redis-module-owners-bicep
96avm/res/cdn/profile46d3xbcp.res.cdn-profileavm-res-cdn-profile-module-owners-bicep
97avm/res/cdn/profile/afd-endpoint46d3xbcp.res.cdn-profile-afdendpointavm-res-cdn-profile-module-owners-bicep
98avm/res/cdn/profile/afd-endpoint/route46d3xbcp.res.cdn-profile-afdendpointrouteavm-res-cdn-profile-module-owners-bicep
99avm/res/cdn/profile/custom-domain46d3xbcp.res.cdn-profile-domainavm-res-cdn-profile-module-owners-bicep
100avm/res/cdn/profile/endpoint46d3xbcp.res.cdn-profile-endpointavm-res-cdn-profile-module-owners-bicep
101avm/res/cdn/profile/endpoint/origin46d3xbcp.res.cdn-profile-endpointoriginavm-res-cdn-profile-module-owners-bicep
102avm/res/cdn/profile/origin-group46d3xbcp.res.cdn-profile-origingroupavm-res-cdn-profile-module-owners-bicep
103avm/res/cdn/profile/origin-group/origin46d3xbcp.res.cdn-profile-origingroup-originavm-res-cdn-profile-module-owners-bicep
104avm/res/cdn/profile/rule-set46d3xbcp.res.cdn-profile-rulesetavm-res-cdn-profile-module-owners-bicep
105avm/res/cdn/profile/rule-set/rule46d3xbcp.res.cdn-profile-rulesetruleavm-res-cdn-profile-module-owners-bicep
106avm/res/cdn/profile/secret46d3xbcp.res.cdn-profile-secretavm-res-cdn-profile-module-owners-bicep
107avm/res/cdn/profile/security-policy46d3xbcp.res.cdn-profile-securitypolicyavm-res-cdn-profile-module-owners-bicep
108avm/res/chaos/experiment46d3xbcp.res.chaos-experimentavm-res-chaos-experiment-module-owners-bicep
109avm/res/code-signing/code-signing-account46d3xbcp.res.codesigning-codesigningaccountavm-res-codesigning-codesigningaccount-module-owners-bicep
110avm/res/cognitive-services/account46d3xbcp.res.cognitiveservices-accountavm-res-cognitiveservices-account-module-owners-bicep
111avm/res/communication/communication-service46d3xbcp.res.communication-communicationserviceavm-res-communication-communicationservice-module-owners-bicep
112avm/res/communication/email-service46d3xbcp.res.communication-emailserviceavm-res-communication-emailservice-module-owners-bicep
113avm/res/communication/email-service/domain46d3xbcp.res.comm-emailservice-domainavm-res-communication-emailservice-module-owners-bicep
114avm/res/communication/email-service/domain/sender-username46d3xbcp.res.comm-emailservice-domsndrusrnameavm-res-communication-emailservice-module-owners-bicep
115avm/res/compute/availability-set46d3xbcp.res.compute-availabilitysetavm-res-compute-availabilityset-module-owners-bicep
116avm/res/compute/disk46d3xbcp.res.compute-diskavm-res-compute-disk-module-owners-bicep
117avm/res/compute/disk-encryption-set46d3xbcp.res.compute-diskencryptionsetavm-res-compute-diskencryptionset-module-owners-bicep
118avm/res/compute/gallery46d3xbcp.res.compute-galleryavm-res-compute-gallery-module-owners-bicep
119avm/res/compute/gallery/application46d3xbcp.res.compute-gallery-applicationavm-res-compute-gallery-module-owners-bicep
120avm/res/compute/gallery/image46d3xbcp.res.compute-gallery-imageavm-res-compute-gallery-module-owners-bicep
121avm/res/compute/image46d3xbcp.res.compute-imageavm-res-compute-image-module-owners-bicep
122avm/res/compute/proximity-placement-group46d3xbcp.res.compute-proximityplacementgroupavm-res-compute-proximityplacementgroup-module-owners-bicep
123avm/res/compute/ssh-public-key46d3xbcp.res.compute-sshpublickeyavm-res-compute-sshpublickey-module-owners-bicep
124avm/res/compute/virtual-machine46d3xbcp.res.compute-virtualmachineavm-res-compute-virtualmachine-module-owners-bicep
125avm/res/compute/virtual-machine-scale-set46d3xbcp.res.compute-virtualmachinescalesetavm-res-compute-virtualmachinescaleset-module-owners-bicep
126avm/res/compute/virtual-machine-scale-set/extension46d3xbcp.res.compute-vmss-extensionavm-res-compute-virtualmachinescaleset-module-owners-bicep
127avm/res/compute/virtual-machine/extension46d3xbcp.res.compute-vm-extensionavm-res-compute-virtualmachine-module-owners-bicep
128avm/res/consumption/budget46d3xbcp.res.consumption-budgetavm-res-consumption-budget-module-owners-bicep
129avm/res/consumption/budget/mg-scope46d3xbcp.res.consumption-budget_mgscopeavm-res-consumption-budget-module-owners-bicep
130avm/res/consumption/budget/rg-scope46d3xbcp.res.consumption-budget_rgscopeavm-res-consumption-budget-module-owners-bicep
131avm/res/consumption/budget/sub-scope46d3xbcp.res.consumption-budget_subscopeavm-res-consumption-budget-module-owners-bicep
132avm/res/container-instance/container-group46d3xbcp.res.containerinstance-containergroupavm-res-containerinstance-containergroup-module-owners-bicep
133avm/res/container-registry/registry46d3xbcp.res.containerregistry-registryavm-res-containerregistry-registry-module-owners-bicep
134avm/res/container-registry/registry/cache-rule46d3xbcp.res.containerregistry-registry-cacheruleavm-res-containerregistry-registry-module-owners-bicep
135avm/res/container-registry/registry/credential-set46d3xbcp.res.containerregistry-registry-credsetavm-res-containerregistry-registry-module-owners-bicep
136avm/res/container-registry/registry/replication46d3xbcp.res.containerregistry-registry-replavm-res-containerregistry-registry-module-owners-bicep
137avm/res/container-registry/registry/scope-map46d3xbcp.res.containerregistry-registry-scopemapavm-res-containerregistry-registry-module-owners-bicep
138avm/res/container-registry/registry/task46d3xbcp.res.containerregistry-registry-taskavm-res-containerregistry-registry-module-owners-bicep
139avm/res/container-registry/registry/token46d3xbcp.res.containerregistry-registry-tokenavm-res-containerregistry-registry-module-owners-bicep
140avm/res/container-registry/registry/webhook46d3xbcp.res.containerregistry-registry-webhookavm-res-containerregistry-registry-module-owners-bicep
141avm/res/container-service/managed-cluster46d3xbcp.res.containerservice-managedclusteravm-res-containerservice-managedcluster-module-owners-bicep
142avm/res/container-service/managed-cluster/agent-pool46d3xbcp.res.consvc-mgdcluster-agentpoolavm-res-containerservice-managedcluster-module-owners-bicep
143avm/res/container-service/managed-cluster/maintenance-configuration46d3xbcp.res.consvc-mgdcluster-maintenancecfgavm-res-containerservice-managedcluster-module-owners-bicep
144avm/res/dashboard/grafana46d3xbcp.res.dashboard-grafanaavm-res-dashboard-grafana-module-owners-bicep
145avm/res/data-factory/factory46d3xbcp.res.datafactory-factoryavm-res-datafactory-factory-module-owners-bicep
146avm/res/data-factory/factory/integration-runtime46d3xbcp.res.datafactory-factory-integrruntimeavm-res-datafactory-factory-module-owners-bicep
147avm/res/data-factory/factory/linked-service46d3xbcp.res.datafactory-factory-linkedserviceavm-res-datafactory-factory-module-owners-bicep
148avm/res/data-factory/factory/managed-virtual-network46d3xbcp.res.datafactory-factory-managedvnetavm-res-datafactory-factory-module-owners-bicep
149avm/res/data-factory/factory/managed-virtual-network/managed-private-endpoint46d3xbcp.res.datafactory-factory-managedvnetmgdpeavm-res-datafactory-factory-module-owners-bicep
150avm/res/data-protection/backup-vault46d3xbcp.res.dataprotection-backupvaultavm-res-dataprotection-backupvault-module-owners-bicep
151avm/res/data-protection/backup-vault/backup-instance46d3xbcp.res.dataprot-backupvault-backupinstavm-res-dataprotection-backupvault-module-owners-bicep
152avm/res/data-protection/backup-vault/backup-policy46d3xbcp.res.dataprot-backupvault-backuppolicyavm-res-dataprotection-backupvault-module-owners-bicep
153avm/res/data-protection/resource-guard46d3xbcp.res.dataprotection-resourceguardavm-res-dataprotection-resourceguard-module-owners-bicep
154avm/res/databricks/access-connector46d3xbcp.res.databricks-accessconnectoravm-res-databricks-accessconnector-module-owners-bicep
155avm/res/databricks/workspace46d3xbcp.res.databricks-workspaceavm-res-databricks-workspace-module-owners-bicep
156avm/res/db-for-my-sql/flexible-server46d3xbcp.res.dbformysql-flexibleserveravm-res-dbformysql-flexibleserver-module-owners-bicep
157avm/res/db-for-my-sql/flexible-server/administrator46d3xbcp.res.dbformysql-flexisrv-administratoravm-res-dbformysql-flexibleserver-module-owners-bicep
158avm/res/db-for-my-sql/flexible-server/advanced-threat-protection46d3xbcp.res.dbformysql-flexisrv-advdthreatprotavm-res-dbformysql-flexibleserver-module-owners-bicep
159avm/res/db-for-my-sql/flexible-server/configuration46d3xbcp.res.dbformysql-flexisrv-configurationavm-res-dbformysql-flexibleserver-module-owners-bicep
160avm/res/db-for-my-sql/flexible-server/database46d3xbcp.res.dbformysql-flexisrv-databaseavm-res-dbformysql-flexibleserver-module-owners-bicep
161avm/res/db-for-my-sql/flexible-server/firewall-rule46d3xbcp.res.dbformysql-flexisrv-firewallruleavm-res-dbformysql-flexibleserver-module-owners-bicep
162avm/res/db-for-postgre-sql/flexible-server46d3xbcp.res.dbforpostgresql-flexibleserveravm-res-dbforpostgresql-flexibleserver-module-owners-bicep
163avm/res/db-for-postgre-sql/flexible-server/administrator46d3xbcp.res.dbforpostgresql-flexisrv-adminavm-res-dbforpostgresql-flexibleserver-module-owners-bicep
164avm/res/db-for-postgre-sql/flexible-server/advanced-threat-protection-setting46d3xbcp.res.dbforpostgresql-flexisrv-adthprotstgavm-res-dbforpostgresql-flexibleserver-module-owners-bicep
165avm/res/db-for-postgre-sql/flexible-server/configuration46d3xbcp.res.dbforpostgresql-flexisrv-configavm-res-dbforpostgresql-flexibleserver-module-owners-bicep
166avm/res/db-for-postgre-sql/flexible-server/database46d3xbcp.res.dbforpostgresql-flexisrv-databaseavm-res-dbforpostgresql-flexibleserver-module-owners-bicep
167avm/res/db-for-postgre-sql/flexible-server/firewall-rule46d3xbcp.res.dbforpostgresql-flexisrv-fwruleavm-res-dbforpostgresql-flexibleserver-module-owners-bicep
168avm/res/desktop-virtualization/application-group46d3xbcp.res.desktopvirtualization-appgroupavm-res-desktopvirtualization-applicationgroup-module-owners-bicep
169avm/res/desktop-virtualization/application-group/application46d3xbcp.res.desktopvirtualization-appgroup-appavm-res-desktopvirtualization-applicationgroup-module-owners-bicep
170avm/res/desktop-virtualization/host-pool46d3xbcp.res.desktopvirtualization-hostpoolavm-res-desktopvirtualization-hostpool-module-owners-bicep
171avm/res/desktop-virtualization/scaling-plan46d3xbcp.res.desktopvirtualization-scalingplanavm-res-desktopvirtualization-scalingplan-module-owners-bicep
172avm/res/desktop-virtualization/workspace46d3xbcp.res.desktopvirtualization-workspaceavm-res-desktopvirtualization-workspace-module-owners-bicep
173avm/res/dev-center/devcenter46d3xbcp.res.devcenter-devcenteravm-res-devcenter-devcenter-module-owners-bicep
174avm/res/dev-center/devcenter/attachednetwork46d3xbcp.res.devcenter-devcenter-attachednetworkavm-res-devcenter-devcenter-module-owners-bicep
175avm/res/dev-center/devcenter/catalog46d3xbcp.res.devcenter-devcenter-catalogavm-res-devcenter-devcenter-module-owners-bicep
176avm/res/dev-center/devcenter/devboxdefinition46d3xbcp.res.devcenter-devcenter-devboxdefinitionavm-res-devcenter-devcenter-module-owners-bicep
177avm/res/dev-center/devcenter/environment-type46d3xbcp.res.devcenter-devcenter-environmenttypeavm-res-devcenter-devcenter-module-owners-bicep
178avm/res/dev-center/devcenter/gallery46d3xbcp.res.devcenter-devcenter-galleryavm-res-devcenter-devcenter-module-owners-bicep
179avm/res/dev-center/devcenter/project-policy46d3xbcp.res.devcenter-devcenter-projectpolicyavm-res-devcenter-devcenter-module-owners-bicep
180avm/res/dev-center/network-connection46d3xbcp.res.devcenter-networkconnectionavm-res-devcenter-networkconnection-module-owners-bicep
181avm/res/dev-center/project46d3xbcp.res.devcenter-projectavm-res-devcenter-project-module-owners-bicep
182avm/res/dev-center/project/catalog46d3xbcp.res.devcenter-project-catalogavm-res-devcenter-project-module-owners-bicep
183avm/res/dev-center/project/environment-type46d3xbcp.res.devcenter-project-environmenttypeavm-res-devcenter-project-module-owners-bicep
184avm/res/dev-center/project/pool46d3xbcp.res.devcenter-project-poolavm-res-devcenter-project-module-owners-bicep
185avm/res/dev-center/project/pool/schedule46d3xbcp.res.devcenter-project-poolscheduleavm-res-devcenter-project-module-owners-bicep
186avm/res/dev-ops-infrastructure/pool46d3xbcp.res.devopsinfrastructure-poolavm-res-devopsinfrastructure-pool-module-owners-bicep
187avm/res/dev-test-lab/lab46d3xbcp.res.devtestlab-labavm-res-devtestlab-lab-module-owners-bicep
188avm/res/dev-test-lab/lab/artifactsource46d3xbcp.res.devtestlab-lab-artifactsourceavm-res-devtestlab-lab-module-owners-bicep
189avm/res/dev-test-lab/lab/cost46d3xbcp.res.devtestlab-lab-costavm-res-devtestlab-lab-module-owners-bicep
190avm/res/dev-test-lab/lab/notificationchannel46d3xbcp.res.devtestlab-lab-notificationchannelavm-res-devtestlab-lab-module-owners-bicep
191avm/res/dev-test-lab/lab/policyset/policy46d3xbcp.res.devtestlab-lab-policysetpolicyavm-res-devtestlab-lab-module-owners-bicep
192avm/res/dev-test-lab/lab/schedule46d3xbcp.res.devtestlab-lab-scheduleavm-res-devtestlab-lab-module-owners-bicep
193avm/res/dev-test-lab/lab/secret46d3xbcp.res.devtestlab-lab-secretavm-res-devtestlab-lab-module-owners-bicep
194avm/res/dev-test-lab/lab/virtualnetwork46d3xbcp.res.devtestlab-lab-virtualnetworkavm-res-devtestlab-lab-module-owners-bicep
195avm/res/devices/iot-hub46d3xbcp.res.devices-iothubavm-res-devices-iothub-module-owners-bicep
196avm/res/digital-twins/digital-twins-instance46d3xbcp.res.digitaltwins-digitaltwinsinstanceavm-res-digitaltwins-digitaltwinsinstance-module-owners-bicep
197avm/res/digital-twins/digital-twins-instance/endpoint46d3xbcp.res.digitaltwins-digitaltwinsinstance-enavm-res-digitaltwins-digitaltwinsinstance-module-owners-bicep
198avm/res/document-db/database-account46d3xbcp.res.documentdb-databaseaccountavm-res-documentdb-databaseaccount-module-owners-bicep
199avm/res/document-db/database-account/cassandra-keyspace46d3xbcp.res.doctdb-dbacct-cassandrkeyspaceavm-res-documentdb-databaseaccount-module-owners-bicep
200avm/res/document-db/database-account/cassandra-keyspace/table46d3xbcp.res.doctdb-dbacct-cassandrkeyspacetableavm-res-documentdb-databaseaccount-module-owners-bicep
201avm/res/document-db/database-account/cassandra-keyspace/view46d3xbcp.res.doctdb-dbacct-cassandrkeyspaceviewavm-res-documentdb-databaseaccount-module-owners-bicep
202avm/res/document-db/database-account/cassandra-role-assignment46d3xbcp.res.doctdb-dbacct-cassandrroleassignmentavm-res-documentdb-databaseaccount-module-owners-bicep
203avm/res/document-db/database-account/cassandra-role-definition46d3xbcp.res.doctdb-dbacct-cassandrroledefinitionavm-res-documentdb-databaseaccount-module-owners-bicep
204avm/res/document-db/database-account/gremlin-database46d3xbcp.res.doctdb-dbacct-gremlindbavm-res-documentdb-databaseaccount-module-owners-bicep
205avm/res/document-db/database-account/gremlin-database/graph46d3xbcp.res.doctdb-dbacct-gremlindbgraphavm-res-documentdb-databaseaccount-module-owners-bicep
206avm/res/document-db/database-account/mongodb-database46d3xbcp.res.doctdb-dbacct-mongodbdatabaseavm-res-documentdb-databaseaccount-module-owners-bicep
207avm/res/document-db/database-account/mongodb-database/collection46d3xbcp.res.doctdb-dbacct-mongodbdbcollectionavm-res-documentdb-databaseaccount-module-owners-bicep
208avm/res/document-db/database-account/sql-database46d3xbcp.res.documentdb-databaseaccountsqldbavm-res-documentdb-databaseaccount-module-owners-bicep
209avm/res/document-db/database-account/sql-database/container46d3xbcp.res.doctdb-dbacct-sqldbcontaineravm-res-documentdb-databaseaccount-module-owners-bicep
210avm/res/document-db/database-account/sql-role-assignment46d3xbcp.res.doctdb-dbacct-sqlroleassignmentavm-res-documentdb-databaseaccount-module-owners-bicep
211avm/res/document-db/database-account/sql-role-definition46d3xbcp.res.doctdb-dbacct-sqlroledefinitionavm-res-documentdb-databaseaccount-module-owners-bicep
212avm/res/document-db/database-account/table46d3xbcp.res.documentdb-databaseaccounttableavm-res-documentdb-databaseaccount-module-owners-bicep
213avm/res/document-db/mongo-cluster46d3xbcp.res.documentdb-mongoclusteravm-res-documentdb-mongocluster-module-owners-bicep
214avm/res/document-db/mongo-cluster/firewall-rule46d3xbcp.res.documentdb-mongocluster-firewallruleavm-res-documentdb-mongocluster-module-owners-bicep
215avm/res/document-db/mongo-cluster/user46d3xbcp.res.documentdb-mongocluster-useravm-res-documentdb-mongocluster-module-owners-bicep
216avm/res/durable-task/scheduler46d3xbcp.res.durabletask-scheduleravm-res-durabletask-scheduler-module-owners-bicep
217avm/res/edge-order/order-item46d3xbcp.res.edgeorder-orderitemavm-res-edgeorder-orderitem-module-owners-bicep
218avm/res/edge/configuration46d3xbcp.res.edge-configurationavm-res-edge-configuration-module-owners-bicep
219avm/res/edge/site46d3xbcp.res.edge-siteavm-res-edge-site-module-owners-bicep
220avm/res/edge/site/rg-scope46d3xbcp.res.edge-site_rgscopeavm-res-edge-site-module-owners-bicep
221avm/res/edge/site/sub-scope46d3xbcp.res.edge-site_subscopeavm-res-edge-site-module-owners-bicep
222avm/res/elastic-san/elastic-san46d3xbcp.res.elasticsan-elasticsanavm-res-elasticsan-elasticsan-module-owners-bicep
223avm/res/elastic-san/elastic-san/volume-group46d3xbcp.res.elsan-elsan-volumegroupavm-res-elasticsan-elasticsan-module-owners-bicep
224avm/res/elastic-san/elastic-san/volume-group/snapshot46d3xbcp.res.elsan-elsan-volumegroupsnapshotavm-res-elasticsan-elasticsan-module-owners-bicep
225avm/res/elastic-san/elastic-san/volume-group/volume46d3xbcp.res.elsan-elsan-volumegroupvolumeavm-res-elasticsan-elasticsan-module-owners-bicep
226avm/res/event-grid/domain46d3xbcp.res.eventgrid-domainavm-res-eventgrid-domain-module-owners-bicep
227avm/res/event-grid/domain/event-subscription46d3xbcp.res.eventgrid-domain-eventsubscriptionavm-res-eventgrid-domain-module-owners-bicep
228avm/res/event-grid/domain/topic46d3xbcp.res.eventgrid-domain-topicavm-res-eventgrid-domain-module-owners-bicep
229avm/res/event-grid/namespace46d3xbcp.res.eventgrid-namespaceavm-res-eventgrid-namespace-module-owners-bicep
230avm/res/event-grid/namespace/ca-certificate46d3xbcp.res.eventgrid-namespace-cacertificateavm-res-eventgrid-namespace-module-owners-bicep
231avm/res/event-grid/namespace/client46d3xbcp.res.eventgrid-namespace-clientavm-res-eventgrid-namespace-module-owners-bicep
232avm/res/event-grid/namespace/client-group46d3xbcp.res.eventgrid-namespace-clientgroupavm-res-eventgrid-namespace-module-owners-bicep
233avm/res/event-grid/namespace/permission-binding46d3xbcp.res.eventgrid-namespace-permbindingavm-res-eventgrid-namespace-module-owners-bicep
234avm/res/event-grid/namespace/topic46d3xbcp.res.eventgrid-namespace-topicavm-res-eventgrid-namespace-module-owners-bicep
235avm/res/event-grid/namespace/topic-space46d3xbcp.res.eventgrid-namespace-topicspaceavm-res-eventgrid-namespace-module-owners-bicep
236avm/res/event-grid/namespace/topic/event-subscription46d3xbcp.res.eventgrid-namespace-topiceventsubavm-res-eventgrid-namespace-module-owners-bicep
237avm/res/event-grid/system-topic46d3xbcp.res.eventgrid-systemtopicavm-res-eventgrid-systemtopic-module-owners-bicep
238avm/res/event-grid/system-topic/event-subscription46d3xbcp.res.eventgrid-systemtopic-eventsubavm-res-eventgrid-systemtopic-module-owners-bicep
239avm/res/event-grid/topic46d3xbcp.res.eventgrid-topicavm-res-eventgrid-topic-module-owners-bicep
240avm/res/event-grid/topic/event-subscription46d3xbcp.res.eventgrid-topic-eventsubavm-res-eventgrid-topic-module-owners-bicep
241avm/res/event-hub/namespace46d3xbcp.res.eventhub-namespaceavm-res-eventhub-namespace-module-owners-bicep
242avm/res/event-hub/namespace/authorization-rule46d3xbcp.res.eventhub-namespace-authorizationruleavm-res-eventhub-namespace-module-owners-bicep
243avm/res/event-hub/namespace/disaster-recovery-config46d3xbcp.res.eventhub-namespace-drconfigavm-res-eventhub-namespace-module-owners-bicep
244avm/res/event-hub/namespace/eventhub46d3xbcp.res.eventhub-nseventhubavm-res-eventhub-namespace-module-owners-bicep
245avm/res/event-hub/namespace/eventhub/authorization-rule46d3xbcp.res.eventhub-namespace-eventhubauthruleavm-res-eventhub-namespace-module-owners-bicep
246avm/res/event-hub/namespace/eventhub/consumergroup46d3xbcp.res.eventhub-namespace-eventhubconsgrpavm-res-eventhub-namespace-module-owners-bicep
247avm/res/event-hub/namespace/network-rule-set46d3xbcp.res.eventhub-namespace-networkrulesetavm-res-eventhub-namespace-module-owners-bicep
248avm/res/fabric/capacity46d3xbcp.res.fabric-capacityavm-res-fabric-capacity-module-owners-bicep
249avm/res/health-bot/health-bot46d3xbcp.res.healthbot-healthbotavm-res-healthbot-healthbot-module-owners-bicep
250avm/res/healthcare-apis/workspace46d3xbcp.res.healthcareapis-workspaceavm-res-healthcareapis-workspace-module-owners-bicep
251avm/res/healthcare-apis/workspace/dicomservice46d3xbcp.res.healthcapis-workspace-dicomservicavm-res-healthcareapis-workspace-module-owners-bicep
252avm/res/healthcare-apis/workspace/fhirservice46d3xbcp.res.healthcapis-workspace-fhirserviceavm-res-healthcareapis-workspace-module-owners-bicep
253avm/res/healthcare-apis/workspace/iotconnector46d3xbcp.res.healthcapis-workspace-iotconnavm-res-healthcareapis-workspace-module-owners-bicep
254avm/res/healthcare-apis/workspace/iotconnector/fhirdestination46d3xbcp.res.healthcapis-workspace-iotconnfhirdstavm-res-healthcareapis-workspace-module-owners-bicep
255avm/res/hybrid-compute/gateway46d3xbcp.res.hybridcompute-gatewayavm-res-hybridcompute-gateway-module-owners-bicep
256avm/res/hybrid-compute/license46d3xbcp.res.hybridcompute-licenseavm-res-hybridcompute-license-module-owners-bicep
257avm/res/hybrid-compute/machine46d3xbcp.res.hybridcompute-machineavm-res-hybridcompute-machine-module-owners-bicep
258avm/res/hybrid-compute/machine/extension46d3xbcp.res.hybridcompute-machine-extensionavm-res-hybridcompute-machine-module-owners-bicep
259avm/res/hybrid-compute/private-link-scope46d3xbcp.res.hybridcompute-privatelinkscopeavm-res-hybridcompute-privatelinkscope-module-owners-bicep
260avm/res/hybrid-compute/setting46d3xbcp.res.hybridcompute-settingavm-res-hybridcompute-setting-module-owners-bicep
261avm/res/hybrid-container-service/provisioned-cluster-instance46d3xbcp.res.hybcontsvc-provclustinstavm-res-hybridcontainerservice-provisionedclusterinstance-module-owners-bicep
262avm/res/insights/action-group46d3xbcp.res.insights-actiongroupavm-res-insights-actiongroup-module-owners-bicep
263avm/res/insights/activity-log-alert46d3xbcp.res.insights-activitylogalertavm-res-insights-activitylogalert-module-owners-bicep
264avm/res/insights/autoscale-setting46d3xbcp.res.insights-autoscalesettingavm-res-insights-autoscalesetting-module-owners-bicep
265avm/res/insights/component46d3xbcp.res.insights-componentavm-res-insights-component-module-owners-bicep
266avm/res/insights/component/linked-storage-account46d3xbcp.res.insights-component-linkedstoracctavm-res-insights-component-module-owners-bicep
267avm/res/insights/data-collection-endpoint46d3xbcp.res.insights-datacollectionendpointavm-res-insights-datacollectionendpoint-module-owners-bicep
268avm/res/insights/data-collection-rule46d3xbcp.res.insights-datacollectionruleavm-res-insights-datacollectionrule-module-owners-bicep
269avm/res/insights/diagnostic-setting46d3xbcp.res.insights-diagnosticsettingavm-res-insights-diagnosticsetting-module-owners-bicep
270avm/res/insights/metric-alert46d3xbcp.res.insights-metricalertavm-res-insights-metricalert-module-owners-bicep
271avm/res/insights/private-link-scope46d3xbcp.res.insights-privatelinkscopeavm-res-insights-privatelinkscope-module-owners-bicep
272avm/res/insights/private-link-scope/scoped-resource46d3xbcp.res.insights-privatelinkscope-scopedresavm-res-insights-privatelinkscope-module-owners-bicep
273avm/res/insights/scheduled-query-rule46d3xbcp.res.insights-scheduledqueryruleavm-res-insights-scheduledqueryrule-module-owners-bicep
274avm/res/insights/webtest46d3xbcp.res.insights-webtestavm-res-insights-webtest-module-owners-bicep
275avm/res/iot-operations/instance46d3xbcp.res.iotoperations-instanceavm-res-iotoperations-instance-module-owners-bicep
276avm/res/key-vault/managed-hsm46d3xbcp.res.keyvault-managedhsmavm-res-keyvault-managedhsm-module-owners-bicep
277avm/res/key-vault/vault46d3xbcp.res.keyvault-vaultavm-res-keyvault-vault-module-owners-bicep
278avm/res/key-vault/vault/access-policy46d3xbcp.res.keyvault-accesspolicyavm-res-keyvault-vault-module-owners-bicep
279avm/res/key-vault/vault/key46d3xbcp.res.keyvault-keyavm-res-keyvault-vault-module-owners-bicep
280avm/res/key-vault/vault/secret46d3xbcp.res.keyvault-secretavm-res-keyvault-vault-module-owners-bicep
281avm/res/kubernetes-configuration/extension46d3xbcp.res.kubernetesconfiguration-extensionavm-res-kubernetesconfiguration-extension-module-owners-bicep
282avm/res/kubernetes-configuration/flux-configuration46d3xbcp.res.kubernetesconfiguration-fluxconfigavm-res-kubernetesconfiguration-fluxconfiguration-module-owners-bicep
283avm/res/kubernetes-runtime/bpg-peer46d3xbcp.res.kubernetesruntime-bgppeeravm-res-kubernetesruntime-bpgpeer-module-owners-bicep
284avm/res/kubernetes-runtime/load-balancer46d3xbcp.res.kubernetesruntime-loadbalanceravm-res-kubernetesruntime-loadbalancer-module-owners-bicep
285avm/res/kubernetes-runtime/service46d3xbcp.res.kubernetesruntime-serviceavm-res-kubernetesruntime-service-module-owners-bicep
286avm/res/kubernetes/connected-cluster46d3xbcp.res.kubernetes-connectedclusteravm-res-kubernetes-connectedcluster-module-owners-bicep
287avm/res/kusto/cluster46d3xbcp.res.kusto-clusteravm-res-kusto-cluster-module-owners-bicep
288avm/res/kusto/cluster/database46d3xbcp.res.kusto-cluster-databaseavm-res-kusto-cluster-module-owners-bicep
289avm/res/kusto/cluster/database/principal-assignment46d3xbcp.res.kusto-cluster-dbprinassignmentavm-res-kusto-cluster-module-owners-bicep
290avm/res/kusto/cluster/principal-assignment46d3xbcp.res.kusto-cluster-principalassignmentavm-res-kusto-cluster-module-owners-bicep
291avm/res/load-test-service/load-test46d3xbcp.res.loadtestservice-loadtestavm-res-loadtestservice-loadtest-module-owners-bicep
292avm/res/logic/integration-account46d3xbcp.res.logic-integrationaccountavm-res-logic-integrationaccount-module-owners-bicep
293avm/res/logic/integration-account/agreement46d3xbcp.res.logic-integrationaccount-agreementavm-res-logic-integrationaccount-module-owners-bicep
294avm/res/logic/integration-account/assembly46d3xbcp.res.logic-integrationaccount-assemblyavm-res-logic-integrationaccount-module-owners-bicep
295avm/res/logic/integration-account/certificate46d3xbcp.res.logic-integrationaccount-certificateavm-res-logic-integrationaccount-module-owners-bicep
296avm/res/logic/integration-account/map46d3xbcp.res.logic-integrationaccount-mapavm-res-logic-integrationaccount-module-owners-bicep
297avm/res/logic/integration-account/partner46d3xbcp.res.logic-integrationaccount-partneravm-res-logic-integrationaccount-module-owners-bicep
298avm/res/logic/integration-account/schema46d3xbcp.res.logic-integrationaccount-schemaavm-res-logic-integrationaccount-module-owners-bicep
299avm/res/logic/workflow46d3xbcp.res.logic-workflowavm-res-logic-workflow-module-owners-bicep
300avm/res/machine-learning-services/registry46d3xbcp.res.machinelearningservices-registryavm-res-machinelearningservices-registry-module-owners-bicep
301avm/res/machine-learning-services/workspace46d3xbcp.res.machinelearningservices-workspaceavm-res-machinelearningservices-workspace-module-owners-bicep
302avm/res/machine-learning-services/workspace/compute46d3xbcp.res.mlservices-workspace-computeavm-res-machinelearningservices-workspace-module-owners-bicep
303avm/res/machine-learning-services/workspace/connection46d3xbcp.res.mlservices-workspace-connectionavm-res-machinelearningservices-workspace-module-owners-bicep
304avm/res/machine-learning-services/workspace/datastore46d3xbcp.res.mlservices-workspace-datastoreavm-res-machinelearningservices-workspace-module-owners-bicep
305avm/res/maintenance/configuration-assignment46d3xbcp.res.maintenance-configurationassignmentavm-res-maintenance-configurationassignment-module-owners-bicep
306avm/res/maintenance/maintenance-configuration46d3xbcp.res.maintenance-maintenanceconfigurationavm-res-maintenance-maintenanceconfiguration-module-owners-bicep
307avm/res/managed-identity/user-assigned-identity46d3xbcp.res.managedidentity-userassignedidentityavm-res-managedidentity-userassignedidentity-module-owners-bicep
308avm/res/managed-identity/user-assigned-identity/federated-identity-credential46d3xbcp.res.managedid-userassignedidcredentialavm-res-managedidentity-userassignedidentity-module-owners-bicep
309avm/res/managed-services/registration-definition46d3xbcp.res.managedservices-registrationdefavm-res-managedservices-registrationdefinition-module-owners-bicep
310avm/res/management/management-group46d3xbcp.res.management-managementgroupavm-res-management-managementgroup-module-owners-bicep
311avm/res/management/service-group46d3xbcp.res.management-servicegroupavm-res-management-servicegroup-module-owners-bicep
312avm/res/maps/account46d3xbcp.res.maps-accountavm-res-maps-account-module-owners-bicep
313avm/res/net-app/net-app-account46d3xbcp.res.netapp-netappaccountavm-res-netapp-netappaccount-module-owners-bicep
314avm/res/net-app/net-app-account/backup-policy46d3xbcp.res.netapp-netappaccount-backuppolicyavm-res-netapp-netappaccount-module-owners-bicep
315avm/res/net-app/net-app-account/backup-vault46d3xbcp.res.netapp-netappaccount-backupvaultavm-res-netapp-netappaccount-module-owners-bicep
316avm/res/net-app/net-app-account/backup-vault/backup46d3xbcp.res.netapp-netappaccount-backupvaultbckpavm-res-netapp-netappaccount-module-owners-bicep
317avm/res/net-app/net-app-account/capacity-pool46d3xbcp.res.netapp-netappaccount-capacitypoolavm-res-netapp-netappaccount-module-owners-bicep
318avm/res/net-app/net-app-account/capacity-pool/volume46d3xbcp.res.netapp-netappaccount-capacitypoolvolavm-res-netapp-netappaccount-module-owners-bicep
319avm/res/net-app/net-app-account/snapshot-policy46d3xbcp.res.netapp-netappaccount-snapshotpolicyavm-res-netapp-netappaccount-module-owners-bicep
320avm/res/network/application-gateway46d3xbcp.res.network-appgwavm-res-network-applicationgateway-module-owners-bicep
321avm/res/network/application-gateway-web-application-firewall-policy46d3xbcp.res.network-appgwwebappfirewallpolicyavm-res-network-applicationgatewaywebapplicationfirewallpolicy-module-owners-bicep
322avm/res/network/application-security-group46d3xbcp.res.network-applicationsecuritygroupavm-res-network-applicationsecuritygroup-module-owners-bicep
323avm/res/network/azure-firewall46d3xbcp.res.network-azurefirewallavm-res-network-azurefirewall-module-owners-bicep
324avm/res/network/bastion-host46d3xbcp.res.network-bastionhostavm-res-network-bastionhost-module-owners-bicep
325avm/res/network/connection46d3xbcp.res.network-connectionavm-res-network-connection-module-owners-bicep
326avm/res/network/ddos-protection-plan46d3xbcp.res.network-ddosprotectionplanavm-res-network-ddosprotectionplan-module-owners-bicep
327avm/res/network/dns-forwarding-ruleset46d3xbcp.res.network-dnsforwardingrulesetavm-res-network-dnsforwardingruleset-module-owners-bicep
328avm/res/network/dns-forwarding-ruleset/forwarding-rule46d3xbcp.res.network-dnsforwardingruleset-fwdruleavm-res-network-dnsforwardingruleset-module-owners-bicep
329avm/res/network/dns-forwarding-ruleset/virtual-network-link46d3xbcp.res.network-dnsfwdruleset-vnetlinkavm-res-network-dnsforwardingruleset-module-owners-bicep
330avm/res/network/dns-resolver46d3xbcp.res.network-dnsresolveravm-res-network-dnsresolver-module-owners-bicep
331avm/res/network/dns-resolver/inbound-endpoint46d3xbcp.res.network-dnsresolver-inboundendpointavm-res-network-dnsresolver-module-owners-bicep
332avm/res/network/dns-resolver/outbound-endpoint46d3xbcp.res.network-dnsresolver-outboundendpointavm-res-network-dnsresolver-module-owners-bicep
333avm/res/network/dns-zone46d3xbcp.res.network-dnszoneavm-res-network-dnszone-module-owners-bicep
334avm/res/network/dns-zone/a46d3xbcp.res.network-dnszone-aavm-res-network-dnszone-module-owners-bicep
335avm/res/network/dns-zone/aaaa46d3xbcp.res.network-dnszone-aaaaavm-res-network-dnszone-module-owners-bicep
336avm/res/network/dns-zone/caa46d3xbcp.res.network-dnszone-caaavm-res-network-dnszone-module-owners-bicep
337avm/res/network/dns-zone/cname46d3xbcp.res.network-dnszone-cnameavm-res-network-dnszone-module-owners-bicep
338avm/res/network/dns-zone/mx46d3xbcp.res.network-dnszone-mxavm-res-network-dnszone-module-owners-bicep
339avm/res/network/dns-zone/ns46d3xbcp.res.network-dnszone-nsavm-res-network-dnszone-module-owners-bicep
340avm/res/network/dns-zone/ptr46d3xbcp.res.network-dnszone-ptravm-res-network-dnszone-module-owners-bicep
341avm/res/network/dns-zone/soa46d3xbcp.res.network-dnszone-soaavm-res-network-dnszone-module-owners-bicep
342avm/res/network/dns-zone/srv46d3xbcp.res.network-dnszone-srvavm-res-network-dnszone-module-owners-bicep
343avm/res/network/dns-zone/txt46d3xbcp.res.network-dnszone-txtavm-res-network-dnszone-module-owners-bicep
344avm/res/network/express-route-circuit46d3xbcp.res.network-expressroutecircuitavm-res-network-expressroutecircuit-module-owners-bicep
345avm/res/network/express-route-gateway46d3xbcp.res.network-expressroutegatewayavm-res-network-expressroutegateway-module-owners-bicep
346avm/res/network/express-route-port46d3xbcp.res.network-expressrouteportavm-res-network-expressrouteport-module-owners-bicep
347avm/res/network/firewall-policy46d3xbcp.res.network-firewallpolicyavm-res-network-firewallpolicy-module-owners-bicep
348avm/res/network/firewall-policy/rule-collection-group46d3xbcp.res.network-fwpolicy-rulecollectionavm-res-network-firewallpolicy-module-owners-bicep
349avm/res/network/front-door46d3xbcp.res.network-frontdooravm-res-network-frontdoor-module-owners-bicep
350avm/res/network/front-door-web-application-firewall-policy46d3xbcp.res.network-frontdoorwebappfwpolicyavm-res-network-frontdoorwebapplicationfirewallpolicy-module-owners-bicep
351avm/res/network/ip-group46d3xbcp.res.network-ipgroupavm-res-network-ipgroup-module-owners-bicep
352avm/res/network/load-balancer46d3xbcp.res.network-loadbalanceravm-res-network-loadbalancer-module-owners-bicep
353avm/res/network/load-balancer/backend-address-pool46d3xbcp.res.network-loadbalancer-beaddresspoolavm-res-network-loadbalancer-module-owners-bicep
354avm/res/network/load-balancer/inbound-nat-rule46d3xbcp.res.network-loadbalancer-inboundnatruleavm-res-network-loadbalancer-module-owners-bicep
355avm/res/network/local-network-gateway46d3xbcp.res.network-localnetworkgatewayavm-res-network-localnetworkgateway-module-owners-bicep
356avm/res/network/nat-gateway46d3xbcp.res.network-natgatewayavm-res-network-natgateway-module-owners-bicep
357avm/res/network/network-interface46d3xbcp.res.network-networkinterfaceavm-res-network-networkinterface-module-owners-bicep
358avm/res/network/network-manager46d3xbcp.res.network-networkmanageravm-res-network-networkmanager-module-owners-bicep
359avm/res/network/network-manager/connectivity-configuration46d3xbcp.res.network-nwmgr-connectivityconfigavm-res-network-networkmanager-module-owners-bicep
360avm/res/network/network-manager/network-group46d3xbcp.res.network-nwmgr-networkgroupavm-res-network-networkmanager-module-owners-bicep
361avm/res/network/network-manager/network-group/static-member46d3xbcp.res.network-nwmgr-nwgrpstaticmemberavm-res-network-networkmanager-module-owners-bicep
362avm/res/network/network-manager/routing-configuration46d3xbcp.res.network-nwmgr-routingcfgavm-res-network-networkmanager-module-owners-bicep
363avm/res/network/network-manager/routing-configuration/rule-collection46d3xbcp.res.network-nwmgr-routingcfgrulecollavm-res-network-networkmanager-module-owners-bicep
364avm/res/network/network-manager/routing-configuration/rule-collection/rule46d3xbcp.res.network-nwmgr-routingcfgrulecollruleavm-res-network-networkmanager-module-owners-bicep
365avm/res/network/network-manager/scope-connection46d3xbcp.res.network-nwmgr-scopeconnectionavm-res-network-networkmanager-module-owners-bicep
366avm/res/network/network-manager/security-admin-configuration46d3xbcp.res.network-nwmgr-secadmcfgavm-res-network-networkmanager-module-owners-bicep
367avm/res/network/network-manager/security-admin-configuration/rule-collection46d3xbcp.res.network-nwmgr-secadmcfgrulecollavm-res-network-networkmanager-module-owners-bicep
368avm/res/network/network-manager/security-admin-configuration/rule-collection/rule46d3xbcp.res.network-nwmgr-secadmcfgrulecollruleavm-res-network-networkmanager-module-owners-bicep
369avm/res/network/network-security-group46d3xbcp.res.network-networksecuritygroupavm-res-network-networksecuritygroup-module-owners-bicep
370avm/res/network/network-security-perimeter46d3xbcp.res.network-nwsecurityperimeteravm-res-network-networksecurityperimeter-module-owners-bicep
371avm/res/network/network-security-perimeter/profile46d3xbcp.res.network-nwsecperim-profileavm-res-network-networksecurityperimeter-module-owners-bicep
372avm/res/network/network-security-perimeter/profile/access-rule46d3xbcp.res.network-nwsecperim-profileaccessruleavm-res-network-networksecurityperimeter-module-owners-bicep
373avm/res/network/network-watcher46d3xbcp.res.network-networkwatcheravm-res-network-networkwatcher-module-owners-bicep
374avm/res/network/network-watcher/connection-monitor46d3xbcp.res.network-networkwatcher-connmonitoravm-res-network-networkwatcher-module-owners-bicep
375avm/res/network/network-watcher/flow-log46d3xbcp.res.network-networkwatcher-flowlogavm-res-network-networkwatcher-module-owners-bicep
376avm/res/network/p2s-vpn-gateway46d3xbcp.res.network-p2svpngatewayavm-res-network-p2svpngateway-module-owners-bicep
377avm/res/network/private-dns-zone46d3xbcp.res.network-privatednszoneavm-res-network-privatednszone-module-owners-bicep
378avm/res/network/private-dns-zone/a46d3xbcp.res.nw-privdnszoneaavm-res-network-privatednszone-module-owners-bicep
379avm/res/network/private-dns-zone/aaaa46d3xbcp.res.nw-privdnszoneaaaaavm-res-network-privatednszone-module-owners-bicep
380avm/res/network/private-dns-zone/cname46d3xbcp.res.nw-privdnszonecnameavm-res-network-privatednszone-module-owners-bicep
381avm/res/network/private-dns-zone/mx46d3xbcp.res.nw-privdnszonemxavm-res-network-privatednszone-module-owners-bicep
382avm/res/network/private-dns-zone/ptr46d3xbcp.res.nw-privdnszoneptravm-res-network-privatednszone-module-owners-bicep
383avm/res/network/private-dns-zone/soa46d3xbcp.res.nw-privdnszonesoaavm-res-network-privatednszone-module-owners-bicep
384avm/res/network/private-dns-zone/srv46d3xbcp.res.nw-privdnszonesrvavm-res-network-privatednszone-module-owners-bicep
385avm/res/network/private-dns-zone/txt46d3xbcp.res.nw-privdnszonetxtavm-res-network-privatednszone-module-owners-bicep
386avm/res/network/private-dns-zone/virtual-network-link46d3xbcp.res.nw-privdnszonevnetlinkavm-res-network-privatednszone-module-owners-bicep
387avm/res/network/private-endpoint46d3xbcp.res.network-privateendpointavm-res-network-privateendpoint-module-owners-bicep
388avm/res/network/private-endpoint/private-dns-zone-group46d3xbcp.res.network-privendpoint-privdnszonegrpavm-res-network-privateendpoint-module-owners-bicep
389avm/res/network/private-link-service46d3xbcp.res.network-privatelinkserviceavm-res-network-privatelinkservice-module-owners-bicep
390avm/res/network/public-ip-address46d3xbcp.res.network-publicipaddressavm-res-network-publicipaddress-module-owners-bicep
391avm/res/network/public-ip-prefix46d3xbcp.res.network-publicipprefixavm-res-network-publicipprefix-module-owners-bicep
392avm/res/network/route-table46d3xbcp.res.network-routetableavm-res-network-routetable-module-owners-bicep
393avm/res/network/service-endpoint-policy46d3xbcp.res.network-serviceendpointpolicyavm-res-network-serviceendpointpolicy-module-owners-bicep
394avm/res/network/trafficmanagerprofile46d3xbcp.res.network-trafficmanagerprofileavm-res-network-trafficmanagerprofile-module-owners-bicep
395avm/res/network/virtual-hub46d3xbcp.res.network-virtualhubavm-res-network-virtualhub-module-owners-bicep
396avm/res/network/virtual-hub/hub-route-table46d3xbcp.res.network-virtualhub-hubroutetableavm-res-network-virtualhub-module-owners-bicep
397avm/res/network/virtual-hub/hub-virtual-network-connection46d3xbcp.res.network-virtualhub-hubvirtualnetworkavm-res-network-virtualhub-module-owners-bicep
398avm/res/network/virtual-hub/route-map46d3xbcp.res.network-virtualhubroutemapavm-res-network-virtualhub-module-owners-bicep
399avm/res/network/virtual-hub/routing-intent46d3xbcp.res.network-virtualhub-routingintentavm-res-network-virtualhub-module-owners-bicep
400avm/res/network/virtual-network46d3xbcp.res.network-virtualnetworkavm-res-network-virtualnetwork-module-owners-bicep
401avm/res/network/virtual-network-gateway46d3xbcp.res.network-virtualnetworkgatewayavm-res-network-virtualnetworkgateway-module-owners-bicep
402avm/res/network/virtual-network-gateway/nat-rule46d3xbcp.res.network-vnetgw-natruleavm-res-network-virtualnetworkgateway-module-owners-bicep
403avm/res/network/virtual-network/subnet46d3xbcp.res.network-virtualnetworksubnetavm-res-network-virtualnetwork-module-owners-bicep
404avm/res/network/virtual-network/virtual-network-peering46d3xbcp.res.network-virtualnetwork-peeringavm-res-network-virtualnetwork-module-owners-bicep
405avm/res/network/virtual-wan46d3xbcp.res.network-virtualwanavm-res-network-virtualwan-module-owners-bicep
406avm/res/network/vpn-gateway46d3xbcp.res.network-vpngatewayavm-res-network-vpngateway-module-owners-bicep
407avm/res/network/vpn-gateway/nat-rule46d3xbcp.res.network-vpngateway-natruleavm-res-network-vpngateway-module-owners-bicep
408avm/res/network/vpn-gateway/vpn-connection46d3xbcp.res.network-vpngateway-vpnconnectionavm-res-network-vpngateway-module-owners-bicep
409avm/res/network/vpn-server-configuration46d3xbcp.res.network-vpnserverconfigurationavm-res-network-vpnserverconfiguration-module-owners-bicep
410avm/res/network/vpn-site46d3xbcp.res.network-vpnsiteavm-res-network-vpnsite-module-owners-bicep
411avm/res/operational-insights/cluster46d3xbcp.res.operationalinsights-clusteravm-res-operationalinsights-cluster-module-owners-bicep
412avm/res/operational-insights/workspace46d3xbcp.res.operationalinsights-workspaceavm-res-operationalinsights-workspace-module-owners-bicep
413avm/res/operational-insights/workspace/data-export46d3xbcp.res.opins-worksp-dataexportavm-res-operationalinsights-workspace-module-owners-bicep
414avm/res/operational-insights/workspace/data-source46d3xbcp.res.opins-worksp-datasourceavm-res-operationalinsights-workspace-module-owners-bicep
415avm/res/operational-insights/workspace/linked-service46d3xbcp.res.opins-worksp-linkedserviceavm-res-operationalinsights-workspace-module-owners-bicep
416avm/res/operational-insights/workspace/linked-storage-account46d3xbcp.res.opins-worksp-linkedstgaccountavm-res-operationalinsights-workspace-module-owners-bicep
417avm/res/operational-insights/workspace/saved-search46d3xbcp.res.opins-worksp-savedsearchavm-res-operationalinsights-workspace-module-owners-bicep
418avm/res/operational-insights/workspace/storage-insight-config46d3xbcp.res.opins-worksp-stginsightconfigavm-res-operationalinsights-workspace-module-owners-bicep
419avm/res/operational-insights/workspace/table46d3xbcp.res.opins-worksp-tableavm-res-operationalinsights-workspace-module-owners-bicep
420avm/res/operations-management/solution46d3xbcp.res.operationsmanagement-solutionavm-res-operationsmanagement-solution-module-owners-bicep
421avm/res/portal/dashboard46d3xbcp.res.portal-dashboardavm-res-portal-dashboard-module-owners-bicep
422avm/res/power-bi-dedicated/capacity46d3xbcp.res.powerbidedicated-capacityavm-res-powerbidedicated-capacity-module-owners-bicep
423avm/res/purview/account46d3xbcp.res.purview-accountavm-res-purview-account-module-owners-bicep
424avm/res/recovery-services/vault46d3xbcp.res.recoveryservices-vaultavm-res-recoveryservices-vault-module-owners-bicep
425avm/res/recovery-services/vault/backup-config46d3xbcp.res.recsvcs-vault-backupconfigavm-res-recoveryservices-vault-module-owners-bicep
426avm/res/recovery-services/vault/backup-fabric/protection-container/protected-item46d3xbcp.res.recsvcs-vault-bckpfabprotcontprotitmavm-res-recoveryservices-vault-module-owners-bicep
427avm/res/recovery-services/vault/backup-policy46d3xbcp.res.recsvcs-vault-backuppolicyavm-res-recoveryservices-vault-module-owners-bicep
428avm/res/recovery-services/vault/replication-alert-setting46d3xbcp.res.recsvcs-vault-replalertsettingavm-res-recoveryservices-vault-module-owners-bicep
429avm/res/recovery-services/vault/replication-fabric46d3xbcp.res.recsvcs-vault-repfabavm-res-recoveryservices-vault-module-owners-bicep
430avm/res/recovery-services/vault/replication-fabric/replication-protection-container46d3xbcp.res.recsvcs-vault-repfabrepprotcontaineravm-res-recoveryservices-vault-module-owners-bicep
431avm/res/recovery-services/vault/replication-fabric/replication-protection-container/replication-protection-container-mapping46d3xbcp.res.recsvcs-vault-repfabrepprotcontmapavm-res-recoveryservices-vault-module-owners-bicep
432avm/res/recovery-services/vault/replication-policy46d3xbcp.res.recsvcs-vault-replicationpolicyavm-res-recoveryservices-vault-module-owners-bicep
433avm/res/relay/namespace46d3xbcp.res.relay-namespaceavm-res-relay-namespace-module-owners-bicep
434avm/res/relay/namespace/authorization-rule46d3xbcp.res.relay-namespace-authorizationruleavm-res-relay-namespace-module-owners-bicep
435avm/res/relay/namespace/hybrid-connection46d3xbcp.res.relay-namespace-hybconnavm-res-relay-namespace-module-owners-bicep
436avm/res/relay/namespace/hybrid-connection/authorization-rule46d3xbcp.res.relay-namespace-hybconnautzruleavm-res-relay-namespace-module-owners-bicep
437avm/res/relay/namespace/network-rule-set46d3xbcp.res.relay-namespace-networkrulesetavm-res-relay-namespace-module-owners-bicep
438avm/res/relay/namespace/wcf-relay46d3xbcp.res.relay-namespace-wcfrelayavm-res-relay-namespace-module-owners-bicep
439avm/res/relay/namespace/wcf-relay/authorization-rule46d3xbcp.res.relay-namespace-wcfrelayauthzruleavm-res-relay-namespace-module-owners-bicep
440avm/res/resource-graph/query46d3xbcp.resourcegraph-queryavm-res-resourcegraph-query-module-owners-bicep
441avm/res/resources/deployment-script46d3xbcp.res.resources-deploymentscriptavm-res-resources-deploymentscript-module-owners-bicep
442avm/res/resources/resource-group46d3xbcp.res.resources-resourcegroupavm-res-resources-resourcegroup-module-owners-bicep
443avm/res/scom/managed-instance46d3xbcp.res.scom-managedinstanceavm-res-scom-managedinstance-module-owners-bicep
444avm/res/search/search-service46d3xbcp.res.search-searchserviceavm-res-search-searchservice-module-owners-bicep
445avm/res/search/search-service/shared-private-link-resource46d3xbcp.res.search-searchsvc-sharedprivlinkresavm-res-search-searchservice-module-owners-bicep
446avm/res/security-insights/data-connector46d3xbcp.res.securityinsights-dataconnectoravm-res-securityinsights-dataconnector-module-owners-bicep
447avm/res/security-insights/onboarding-state46d3xbcp.res.securityinsights-onboardingstateavm-res-securityinsights-onboardingstate-module-owners-bicep
448avm/res/security-insights/setting46d3xbcp.res.securityinsights-settingavm-res-securityinsights-setting-module-owners-bicep
449avm/res/service-bus/namespace46d3xbcp.res.servicebus-namespaceavm-res-servicebus-namespace-module-owners-bicep
450avm/res/service-bus/namespace/authorization-rule46d3xbcp.res.servicebus-namespace-authzruleavm-res-servicebus-namespace-module-owners-bicep
451avm/res/service-bus/namespace/disaster-recovery-config46d3xbcp.res.servicebus-namespace-drconfigavm-res-servicebus-namespace-module-owners-bicep
452avm/res/service-bus/namespace/migration-configuration46d3xbcp.res.servicebus-namespace-migrconfigavm-res-servicebus-namespace-module-owners-bicep
453avm/res/service-bus/namespace/network-rule-set46d3xbcp.res.servicebus-namespace-netrulesetavm-res-servicebus-namespace-module-owners-bicep
454avm/res/service-bus/namespace/queue46d3xbcp.res.servicebus-namespacequeueavm-res-servicebus-namespace-module-owners-bicep
455avm/res/service-bus/namespace/queue/authorization-rule46d3xbcp.res.servicebus-namespace-queueauthzruleavm-res-servicebus-namespace-module-owners-bicep
456avm/res/service-bus/namespace/topic46d3xbcp.res.servicebus-namespacetopicavm-res-servicebus-namespace-module-owners-bicep
457avm/res/service-bus/namespace/topic/authorization-rule46d3xbcp.res.servicebus-namespace-topicauthzruleavm-res-servicebus-namespace-module-owners-bicep
458avm/res/service-bus/namespace/topic/subscription46d3xbcp.res.servicebus-namespace-topicsubavm-res-servicebus-namespace-module-owners-bicep
459avm/res/service-bus/namespace/topic/subscription/rule46d3xbcp.res.servicebus-namespace-topicsubruleavm-res-servicebus-namespace-module-owners-bicep
460avm/res/service-fabric/cluster46d3xbcp.res.servicefabric-clusteravm-res-servicefabric-cluster-module-owners-bicep
461avm/res/service-fabric/cluster/application-type46d3xbcp.res.servicefabric-cluster-apptypeavm-res-servicefabric-cluster-module-owners-bicep
462avm/res/service-networking/traffic-controller46d3xbcp.res.servicenetworking-trafficcontrolleravm-res-servicenetworking-trafficcontroller-module-owners-bicep
463avm/res/service-networking/traffic-controller/association46d3xbcp.res.servicenetworking-traffctrlassocavm-res-servicenetworking-trafficcontroller-module-owners-bicep
464avm/res/service-networking/traffic-controller/frontend46d3xbcp.res.servicenetworking-traffctrlfrontendavm-res-servicenetworking-trafficcontroller-module-owners-bicep
465avm/res/service-networking/traffic-controller/security-policy46d3xbcp.res.servicenetworking-traffctrlsecpolicyavm-res-servicenetworking-trafficcontroller-module-owners-bicep
466avm/res/signal-r-service/signal-r46d3xbcp.res.signalrservice-signalravm-res-signalrservice-signalr-module-owners-bicep
467avm/res/signal-r-service/web-pub-sub46d3xbcp.res.signalrservice-webpubsubavm-res-signalrservice-webpubsub-module-owners-bicep
468avm/res/sql-virtual-machine/sql-virtual-machine46d3xbcp.res.sqlvm-sqlvmavm-res-sqlvirtualmachine-sqlvirtualmachine-module-owners-bicep
469avm/res/sql/instance-pool46d3xbcp.res.sql-instancepoolavm-res-sql-instancepool-module-owners-bicep
470avm/res/sql/managed-instance46d3xbcp.res.sql-managedinstanceavm-res-sql-managedinstance-module-owners-bicep
471avm/res/sql/managed-instance/database46d3xbcp.res.sql-mi-databaseavm-res-sql-managedinstance-module-owners-bicep
472avm/res/sql/managed-instance/database/backup-long-term-retention-policy46d3xbcp.res.sql-mi-dbbckplongermretpolicyavm-res-sql-managedinstance-module-owners-bicep
473avm/res/sql/managed-instance/database/backup-short-term-retention-policy46d3xbcp.res.sql-mi-dbbckpshortermretpolicyavm-res-sql-managedinstance-module-owners-bicep
474avm/res/sql/managed-instance/encryption-protector46d3xbcp.res.sql-mi-encryptionprotecoravm-res-sql-managedinstance-module-owners-bicep
475avm/res/sql/managed-instance/key46d3xbcp.res.sql-mi-keyavm-res-sql-managedinstance-module-owners-bicep
476avm/res/sql/managed-instance/security-alert-policy46d3xbcp.res.sql-mi-securityalertpolicyavm-res-sql-managedinstance-module-owners-bicep
477avm/res/sql/managed-instance/vulnerability-assessment46d3xbcp.res.sql-mi-vulnerabilityassessmentavm-res-sql-managedinstance-module-owners-bicep
478avm/res/sql/server46d3xbcp.res.sql-serveravm-res-sql-server-module-owners-bicep
479avm/res/sql/server/auditing-setting46d3xbcp.res.sql-server-auditingsettingavm-res-sql-server-module-owners-bicep
480avm/res/sql/server/database46d3xbcp.res.sql-serverdbavm-res-sql-server-module-owners-bicep
481avm/res/sql/server/database/backup-long-term-retention-policy46d3xbcp.res.sql-server-dbbckplongtermretpolicyavm-res-sql-server-module-owners-bicep
482avm/res/sql/server/database/backup-short-term-retention-policy46d3xbcp.res.sql-server-dbbckpshorttermretpolicyavm-res-sql-server-module-owners-bicep
483avm/res/sql/server/elastic-pool46d3xbcp.res.sql-server-elasticpoolavm-res-sql-server-module-owners-bicep
484avm/res/sql/server/encryption-protector46d3xbcp.res.sql-server-encryptionprotectoravm-res-sql-server-module-owners-bicep
485avm/res/sql/server/failover-group46d3xbcp.res.sql-server-failovergroupavm-res-sql-server-module-owners-bicep
486avm/res/sql/server/firewall-rule46d3xbcp.res.sql-server-firewallruleavm-res-sql-server-module-owners-bicep
487avm/res/sql/server/key46d3xbcp.res.sql-server-keyavm-res-sql-server-module-owners-bicep
488avm/res/sql/server/security-alert-policy46d3xbcp.res.sql-server-securityalertpolicyavm-res-sql-server-module-owners-bicep
489avm/res/sql/server/virtual-network-rule46d3xbcp.res.sql-server-virtualnetworkruleavm-res-sql-server-module-owners-bicep
490avm/res/sql/server/vulnerability-assessment46d3xbcp.res.sql-server-vulnerabilityassessmentavm-res-sql-server-module-owners-bicep
491avm/res/storage/storage-account46d3xbcp.res.storage-storageaccountavm-res-storage-storageaccount-module-owners-bicep
492avm/res/storage/storage-account/blob-service46d3xbcp.res.storage-storageaccount-blobserviceavm-res-storage-storageaccount-module-owners-bicep
493avm/res/storage/storage-account/blob-service/container46d3xbcp.res.storage-blobcontaineravm-res-storage-storageaccount-module-owners-bicep
494avm/res/storage/storage-account/blob-service/container/immutability-policy46d3xbcp.res.storage-containerimmutpolicyavm-res-storage-storageaccount-module-owners-bicep
495avm/res/storage/storage-account/file-service46d3xbcp.res.storage-storageaccount-fileserviceavm-res-storage-storageaccount-module-owners-bicep
496avm/res/storage/storage-account/file-service/share46d3xbcp.res.storage-fileshareavm-res-storage-storageaccount-module-owners-bicep
497avm/res/storage/storage-account/local-user46d3xbcp.res.storage-localuseravm-res-storage-storageaccount-module-owners-bicep
498avm/res/storage/storage-account/management-policy46d3xbcp.res.storage-mgmtpolicyavm-res-storage-storageaccount-module-owners-bicep
499avm/res/storage/storage-account/object-replication-policy46d3xbcp.res.storage-storacct-objectreplpolicyavm-res-storage-storageaccount-module-owners-bicep
500avm/res/storage/storage-account/object-replication-policy/policy46d3xbcp.res.storage-storacct-objectreplpolicypolavm-res-storage-storageaccount-module-owners-bicep
501avm/res/storage/storage-account/queue-service46d3xbcp.res.storage-storageaccount-queueserviceavm-res-storage-storageaccount-module-owners-bicep
502avm/res/storage/storage-account/queue-service/queue46d3xbcp.res.storage-queueavm-res-storage-storageaccount-module-owners-bicep
503avm/res/storage/storage-account/table-service46d3xbcp.res.storage-storageaccount-tableserviceavm-res-storage-storageaccount-module-owners-bicep
504avm/res/storage/storage-account/table-service/table46d3xbcp.res.storage-tableavm-res-storage-storageaccount-module-owners-bicep
505avm/res/stream-analytics/streaming-job46d3xbcp.res.streamanalytics-streamingjobavm-res-streamanalytics-streamingjob-module-owners-bicep
506avm/res/synapse/private-link-hub46d3xbcp.res.synapse-privatelinkhubavm-res-synapse-privatelinkhub-module-owners-bicep
507avm/res/synapse/workspace46d3xbcp.res.synapse-workspaceavm-res-synapse-workspace-module-owners-bicep
508avm/res/synapse/workspace/administrator46d3xbcp.res.synapse-workspace-administratoravm-res-synapse-workspace-module-owners-bicep
509avm/res/synapse/workspace/big-data-pool46d3xbcp.res.synapse-workspace-bigdatapoolavm-res-synapse-workspace-module-owners-bicep
510avm/res/synapse/workspace/firewall-rule46d3xbcp.res.synapse-workspace-firewallruleavm-res-synapse-workspace-module-owners-bicep
511avm/res/synapse/workspace/integration-runtime46d3xbcp.res.synapse-workspace-integrationruntimeavm-res-synapse-workspace-module-owners-bicep
512avm/res/synapse/workspace/key46d3xbcp.res.synapse-workspace-keyavm-res-synapse-workspace-module-owners-bicep
513avm/res/synapse/workspace/sql-pool46d3xbcp.res.synapse-workspace-sqlpoolavm-res-synapse-workspace-module-owners-bicep
514avm/res/virtual-machine-images/image-template46d3xbcp.res.virtualmachineimages-imagetemplateavm-res-virtualmachineimages-imagetemplate-module-owners-bicep
515avm/res/web/connection46d3xbcp.res.web-connectionavm-res-web-connection-module-owners-bicep
516avm/res/web/hosting-environment46d3xbcp.res.web-hostingenvironmentavm-res-web-hostingenvironment-module-owners-bicep
517avm/res/web/hosting-environment/configuration-customdnssuffix46d3xbcp.res.web-hostingenvironment-configurationavm-res-web-hostingenvironment-module-owners-bicep
518avm/res/web/serverfarm46d3xbcp.res.web-serverfarmavm-res-web-serverfarm-module-owners-bicep
519avm/res/web/site46d3xbcp.res.web-siteavm-res-web-site-module-owners-bicep
520avm/res/web/site/basic-publishing-credentials-policy46d3xbcp.res.web-site-basicpublishingcredpolicyavm-res-web-site-module-owners-bicep
521avm/res/web/site/config46d3xbcp.res.web-siteconfigavm-res-web-site-module-owners-bicep
522avm/res/web/site/extension46d3xbcp.res.web-site-extensionavm-res-web-site-module-owners-bicep
523avm/res/web/site/hybrid-connection-namespace/relay46d3xbcp.res.web-site-hybconnnamespacerelayavm-res-web-site-module-owners-bicep
524avm/res/web/site/slot46d3xbcp.res.web-siteslotavm-res-web-site-module-owners-bicep
525avm/res/web/site/slot/basic-publishing-credentials-policy46d3xbcp.res.web-site-slotbasicpubcredpolicyavm-res-web-site-module-owners-bicep
526avm/res/web/site/slot/config46d3xbcp.res.web-site-slotconfigavm-res-web-site-module-owners-bicep
527avm/res/web/site/slot/extension46d3xbcp.res.web-site-slotextensionavm-res-web-site-module-owners-bicep
528avm/res/web/site/slot/hybrid-connection-namespace/relay46d3xbcp.res.web-site-slothybconnnamespacerelayavm-res-web-site-module-owners-bicep
529avm/res/web/static-site46d3xbcp.res.web-staticsiteavm-res-web-staticsite-module-owners-bicep
530avm/res/web/static-site/config46d3xbcp.res.web-staticsite-configavm-res-web-staticsite-module-owners-bicep
531avm/res/web/static-site/custom-domain46d3xbcp.res.web-staticsite-customdomainavm-res-web-staticsite-module-owners-bicep
532avm/res/web/static-site/linked-backend46d3xbcp.res.web-staticsite-linkedbackendavm-res-web-staticsite-module-owners-bicep

Bicep Pattern Modules

Module catalog

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
BicepPattern412162
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Info

This page contains various views of the module index (catalog) for Bicep Pattern Modules. To see these views, click on the expandable sections with the “βž•” sign below.

  • To see the full, unfiltered, unformatted module index on GitHub, click here.

  • To download the source CSV file, click here.

Note

Modules listed below that aren’t shown with the status of Module Available 🟒, are currently in development and are not yet available for use. For proposed modules, see the Proposed modules section below.

Published modules - 🟒 & 🟑

βž• Published Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/aca-lza/hosting-environmentAzure Container Apps (ACA) LZA - Hosting Environmentkpantos
Konstantinos Pantos
02avm/ptn/ai-ml/ai-foundryAI-ML - AI Foundrymswantek68
Mike Swantek
sethsteenken
Seth Steenken
03avm/ptn/ai-platform/baselineAI Platform - Baselinececheta
Chinedum Echeta
ross-p-smith
Ross Smith
04avm/ptn/alz/amaAzure Landing Zones (ALZ) - Azure Monitoring Agent (AMA)oZakari
Zach Trocinski
05avm/ptn/alz/emptyAzure Landing Zones (ALZ) - Emptyjtracey93
Jack Tracey
oZakari
Zach Trocinski
06avm/ptn/app-service-lza/hosting-environmentApp Service LZA - Hosting EnvironmentMikeTB-Microsoft
Michael Baker
07avm/ptn/app/container-job-toolkitContainer Job ToolkitReneHezser
Rene Hezser
08avm/ptn/app/iaas-vm-cosmosdb-tier4Workload - IaaS VM Cosmos DB - Tier 4mikestiers
Mike Stiers
09avm/ptn/authorization/pim-role-assignmentAuthorization - PIM Role Assignmentsebassem
Seif Bassem
10avm/ptn/authorization/policy-assignmentAuthorization - Policy Assignmentjtracey93
Jack Tracey
11avm/ptn/authorization/policy-exemptionAuthorization - Policy ExemptionoZakari
Zach Trocinski
12avm/ptn/authorization/resource-role-assignmentAuthorization - Resource Role Assignmentpeterbud
Peter Budai
13avm/ptn/authorization/role-assignmentAuthorization - Role Assignmentjtracey93
Jack Tracey
14avm/ptn/authorization/role-definitionAuthorization - Role Definitionjtracey93
Jack Tracey
15avm/ptn/azd/acr-container-appAZD - ACR Container App
Azure Developer CLI - ACR Container App
JeffreyCA
Jeffrey Chen
16avm/ptn/azd/aksAZD - AKS
Azure Developer CLI - Azure Kubernetes Services
JeffreyCA
Jeffrey Chen
17avm/ptn/azd/aks-automatic-clusterAZD - AKS Automatic Cluster
Azure Developer CLI - Azure Kubernetes Services Automatic Cluster
JeffreyCA
Jeffrey Chen
18avm/ptn/azd/apim-apiAZD - APIM API
Azure Developer CLI - API Management
JeffreyCA
Jeffrey Chen
19avm/ptn/azd/container-app-upsertAZD - Container App Upsert
Azure Developer CLI - Container Apps Upsert
JeffreyCA
Jeffrey Chen
20avm/ptn/azd/container-apps-stackAZD - Container Apps Stack
Azure Developer CLI - Container Apps Stack
JeffreyCA
Jeffrey Chen
21avm/ptn/azd/insights-dashboardAZD - Insights Dashboard
Azure Developer CLI - Inishgts Dashboard
JeffreyCA
Jeffrey Chen
22avm/ptn/azd/monitoringAZD - Monitoring
Azure Developer CLI - Monitoring
JeffreyCA
Jeffrey Chen
23avm/ptn/data/private-analytical-workspacePrivate Analytical Workspace
Data Analytics, Data Lake, Databricks, Database
jbinko
Jiri Binko
24avm/ptn/deployment-script/import-image-to-acrDeployment Script - Import Container Image to ACRReneHezser
Rene Hezser
25avm/ptn/dev-ops/cicd-agents-and-runnersAzure DevOps and GitHub CI/CD Agents and Runnerssebassem
Seif Bassem
26avm/ptn/finops-toolkit/finops-hubFinOps Toolkit - FinOps HubFallenHoot
Zach Olinske
27avm/ptn/lz/sub-vendingLanding Zone Subscription Vendingjtracey93
Jack Tracey
sebassem
Seif Bassem
28avm/ptn/mgmt-groups/subscription-placementManagement Groups - Subscription PlacementoZakari
Zach Trocinski
29avm/ptn/network/hub-networkingHub NetworkingoZakari
Zach Trocinski
jtracey93
Jack Tracey
30avm/ptn/network/private-link-private-dns-zonesPrivate Link Private DNS Zonesjtracey93
Jack Tracey
31avm/ptn/network/virtual-wanVirtual WAN
vWAN
ericscheffler
Eric Scheffler
juancj
Juan Jimenez
32avm/ptn/policy-insights/remediationPolicy Insights Remediationdonk-msft
Don Koning
33avm/ptn/sa/chat-with-your-dataSA - Chat with your data
Solution Accelerator - Chat with your data (CWYD)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
34avm/ptn/sa/content-processingSA - Content Processing
Solution Accelerator - Content Processing
brittneek
Brittnee Keller
35avm/ptn/sa/conversation-knowledge-miningSA - Conversation knowledge mining
Solution Accelerator - Conversation knowledge mining (CKM)
alguadam
Alvaro Guadamillas Herranz
36avm/ptn/sa/document-knowledge-miningSA - Document knowledge mining
Solution Accelerator - Document knowledge mining (DKM)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
37avm/ptn/sa/modernize-your-codeSA - Modernize your code
Solution Accelerator - Modernize your code
sethsteenken
Seth Steenken
38avm/ptn/sa/multi-agent-custom-automation-engineSA - Multi Agent Custom Automation Engine
Solution Accelerator - Multi Agent Custom Automation Engine
alguadam
Alvaro Guadamillas Herranz
39avm/ptn/security/security-centerAzure Security Center (Defender for Cloud)oZakari
Zach Trocinski
jtracey93
Jack Tracey
40avm/ptn/subscription/service-health-alertsService Health Alertssebassem
Seif Bassem
41avm/ptn/virtual-machine-images/azure-image-builderCustom Images using Azure Image BuilderAlexanderSehr
Alexander Sehr

Proposed modules - βšͺ

βž• Proposed Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/ai-ml/landing-zoneAI-ML - Landing Zone (LZ)placerda
Paulo Lacerda
mbilalamjad
Bilal Amjad
02avm/ptn/app/cosmos-db-account-container-appCosmos DB Account - Container App
03avm/ptn/app/mongodb-cluster-container-appMongoDB Cluster - Container App
04avm/ptn/app/paas-ase-cosmosdb-tier4Workload - PaaS ASE Cosmos DB - Tier 4mikestiers
Mike Stiers
05avm/ptn/avd-lza/insightsAzure Virtual Desktop (AVD) LZA - Insights
06avm/ptn/avd-lza/management-planeAzure Virtual Desktop (AVD) LZA - Management Plane
07avm/ptn/avd-lza/networkingAzure Virtual Desktop (AVD) LZA - Networking
08avm/ptn/avd-lza/session-hostsAzure Virtual Desktop (AVD) LZA - Session Hosts
09avm/ptn/deployment-script/create-kv-ssh-key-pairDeployment Script - Create Key Vault SSH Key Pairvlahane
Vishal Lahane
10avm/ptn/deployment-script/privateDeployment Script - Private Scriptsebassem
Seif Bassem
11avm/ptn/dev-center/dev-boxDev-Boxtimfurnival-MSFT
Tim Furnival
12avm/ptn/lza-shared/data-servicesLZA Shared - Data Services
Landing Zone Accelerators - Shared - Data Services
kpantos
Konstantinos Pantos
13avm/ptn/maintenance/azure-update-managerAzure Update Managerakhilthomas011
Akhil Thomas
14avm/ptn/monitoring/ambaAzure Monitor Baseline Alerts (AMBA)arjenhuitema
Arjen Huitema
15avm/ptn/monitoring/amba-alzAzure Monitor Baseline Alerts (AMBA) - ALZarjenhuitema
Arjen Huitema
16avm/ptn/network/vwan-connected-vnetsVNETs peered to Virtual WANjuancj
Juan Jimenez
ericscheffler
Eric Scheffler
17avm/ptn/openai/cognitive-searchCorporate Line of Business (LoB) ChatBotandbron
Andrew Lambert
18avm/ptn/openai/e2e-baselineAzure OpenAI End-to-End Baseline Implementation
19avm/ptn/sa/content-generationSA - Content Generation
Solution Accelerator - Content Generation
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
20avm/ptn/sa/customer-chatbotSA - Bring your own Customer Chatbot
Solution Accelerator - Bring your own Customer Chatbot (BYOCC)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
21avm/ptn/security/sentinelSentinel

Deprecated modules - πŸ”΄

βž• Deprecated Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/azd/ml-ai-environmentAZD - ML AI Environment
Azure Developer CLI - Machine Learning AI Environment

02avm/ptn/azd/ml-hub-dependenciesAZD - ML Hub Dependencies
Azure Developer CLI - Machine Learning Hub Dependencies

03avm/ptn/azd/ml-projectAZD - ML Project
Azure Developer CLI - Machine Learning Project

04avm/ptn/sa/build-your-own-copilotSA - Build your own Copilot
Solution Accelerator - Build your own Copilot (BYOC)


All modules - πŸ“‡

βž• All Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/aca-lza/hosting-environmentAzure Container Apps (ACA) LZA - Hosting Environmentkpantos
Konstantinos Pantos
02avm/ptn/ai-ml/ai-foundryAI-ML - AI Foundrymswantek68
Mike Swantek
sethsteenken
Seth Steenken
03avm/ptn/ai-ml/landing-zoneAI-ML - Landing Zone (LZ)placerda
Paulo Lacerda
mbilalamjad
Bilal Amjad
04avm/ptn/ai-platform/baselineAI Platform - Baselinececheta
Chinedum Echeta
ross-p-smith
Ross Smith
05avm/ptn/alz/amaAzure Landing Zones (ALZ) - Azure Monitoring Agent (AMA)oZakari
Zach Trocinski
06avm/ptn/alz/emptyAzure Landing Zones (ALZ) - Emptyjtracey93
Jack Tracey
oZakari
Zach Trocinski
07avm/ptn/app-service-lza/hosting-environmentApp Service LZA - Hosting EnvironmentMikeTB-Microsoft
Michael Baker
08avm/ptn/app/container-job-toolkitContainer Job ToolkitReneHezser
Rene Hezser
09avm/ptn/app/cosmos-db-account-container-appCosmos DB Account - Container App
10avm/ptn/app/iaas-vm-cosmosdb-tier4Workload - IaaS VM Cosmos DB - Tier 4mikestiers
Mike Stiers
11avm/ptn/app/mongodb-cluster-container-appMongoDB Cluster - Container App
12avm/ptn/app/paas-ase-cosmosdb-tier4Workload - PaaS ASE Cosmos DB - Tier 4mikestiers
Mike Stiers
13avm/ptn/authorization/pim-role-assignmentAuthorization - PIM Role Assignmentsebassem
Seif Bassem
14avm/ptn/authorization/policy-assignmentAuthorization - Policy Assignmentjtracey93
Jack Tracey
15avm/ptn/authorization/policy-exemptionAuthorization - Policy ExemptionoZakari
Zach Trocinski
16avm/ptn/authorization/resource-role-assignmentAuthorization - Resource Role Assignmentpeterbud
Peter Budai
17avm/ptn/authorization/role-assignmentAuthorization - Role Assignmentjtracey93
Jack Tracey
18avm/ptn/authorization/role-definitionAuthorization - Role Definitionjtracey93
Jack Tracey
19avm/ptn/avd-lza/insightsAzure Virtual Desktop (AVD) LZA - Insights
20avm/ptn/avd-lza/management-planeAzure Virtual Desktop (AVD) LZA - Management Plane
21avm/ptn/avd-lza/networkingAzure Virtual Desktop (AVD) LZA - Networking
22avm/ptn/avd-lza/session-hostsAzure Virtual Desktop (AVD) LZA - Session Hosts
23avm/ptn/azd/acr-container-appAZD - ACR Container App
Azure Developer CLI - ACR Container App
JeffreyCA
Jeffrey Chen
24avm/ptn/azd/aksAZD - AKS
Azure Developer CLI - Azure Kubernetes Services
JeffreyCA
Jeffrey Chen
25avm/ptn/azd/aks-automatic-clusterAZD - AKS Automatic Cluster
Azure Developer CLI - Azure Kubernetes Services Automatic Cluster
JeffreyCA
Jeffrey Chen
26avm/ptn/azd/apim-apiAZD - APIM API
Azure Developer CLI - API Management
JeffreyCA
Jeffrey Chen
27avm/ptn/azd/container-app-upsertAZD - Container App Upsert
Azure Developer CLI - Container Apps Upsert
JeffreyCA
Jeffrey Chen
28avm/ptn/azd/container-apps-stackAZD - Container Apps Stack
Azure Developer CLI - Container Apps Stack
JeffreyCA
Jeffrey Chen
29avm/ptn/azd/insights-dashboardAZD - Insights Dashboard
Azure Developer CLI - Inishgts Dashboard
JeffreyCA
Jeffrey Chen
30avm/ptn/azd/ml-ai-environmentAZD - ML AI Environment
Azure Developer CLI - Machine Learning AI Environment

31avm/ptn/azd/ml-hub-dependenciesAZD - ML Hub Dependencies
Azure Developer CLI - Machine Learning Hub Dependencies

32avm/ptn/azd/ml-projectAZD - ML Project
Azure Developer CLI - Machine Learning Project

33avm/ptn/azd/monitoringAZD - Monitoring
Azure Developer CLI - Monitoring
JeffreyCA
Jeffrey Chen
34avm/ptn/data/private-analytical-workspacePrivate Analytical Workspace
Data Analytics, Data Lake, Databricks, Database
jbinko
Jiri Binko
35avm/ptn/deployment-script/create-kv-ssh-key-pairDeployment Script - Create Key Vault SSH Key Pairvlahane
Vishal Lahane
36avm/ptn/deployment-script/import-image-to-acrDeployment Script - Import Container Image to ACRReneHezser
Rene Hezser
37avm/ptn/deployment-script/privateDeployment Script - Private Scriptsebassem
Seif Bassem
38avm/ptn/dev-center/dev-boxDev-Boxtimfurnival-MSFT
Tim Furnival
39avm/ptn/dev-ops/cicd-agents-and-runnersAzure DevOps and GitHub CI/CD Agents and Runnerssebassem
Seif Bassem
40avm/ptn/finops-toolkit/finops-hubFinOps Toolkit - FinOps HubFallenHoot
Zach Olinske
41avm/ptn/lz/sub-vendingLanding Zone Subscription Vendingjtracey93
Jack Tracey
sebassem
Seif Bassem
42avm/ptn/lza-shared/data-servicesLZA Shared - Data Services
Landing Zone Accelerators - Shared - Data Services
kpantos
Konstantinos Pantos
43avm/ptn/maintenance/azure-update-managerAzure Update Managerakhilthomas011
Akhil Thomas
44avm/ptn/mgmt-groups/subscription-placementManagement Groups - Subscription PlacementoZakari
Zach Trocinski
45avm/ptn/monitoring/ambaAzure Monitor Baseline Alerts (AMBA)arjenhuitema
Arjen Huitema
46avm/ptn/monitoring/amba-alzAzure Monitor Baseline Alerts (AMBA) - ALZarjenhuitema
Arjen Huitema
47avm/ptn/network/hub-networkingHub NetworkingoZakari
Zach Trocinski
jtracey93
Jack Tracey
48avm/ptn/network/private-link-private-dns-zonesPrivate Link Private DNS Zonesjtracey93
Jack Tracey
49avm/ptn/network/virtual-wanVirtual WAN
vWAN
ericscheffler
Eric Scheffler
juancj
Juan Jimenez
50avm/ptn/network/vwan-connected-vnetsVNETs peered to Virtual WANjuancj
Juan Jimenez
ericscheffler
Eric Scheffler
51avm/ptn/openai/cognitive-searchCorporate Line of Business (LoB) ChatBotandbron
Andrew Lambert
52avm/ptn/openai/e2e-baselineAzure OpenAI End-to-End Baseline Implementation
53avm/ptn/policy-insights/remediationPolicy Insights Remediationdonk-msft
Don Koning
54avm/ptn/sa/build-your-own-copilotSA - Build your own Copilot
Solution Accelerator - Build your own Copilot (BYOC)

55avm/ptn/sa/chat-with-your-dataSA - Chat with your data
Solution Accelerator - Chat with your data (CWYD)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
56avm/ptn/sa/content-generationSA - Content Generation
Solution Accelerator - Content Generation
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
57avm/ptn/sa/content-processingSA - Content Processing
Solution Accelerator - Content Processing
brittneek
Brittnee Keller
58avm/ptn/sa/conversation-knowledge-miningSA - Conversation knowledge mining
Solution Accelerator - Conversation knowledge mining (CKM)
alguadam
Alvaro Guadamillas Herranz
59avm/ptn/sa/customer-chatbotSA - Bring your own Customer Chatbot
Solution Accelerator - Bring your own Customer Chatbot (BYOCC)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
60avm/ptn/sa/document-knowledge-miningSA - Document knowledge mining
Solution Accelerator - Document knowledge mining (DKM)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
61avm/ptn/sa/modernize-your-codeSA - Modernize your code
Solution Accelerator - Modernize your code
sethsteenken
Seth Steenken
62avm/ptn/sa/multi-agent-custom-automation-engineSA - Multi Agent Custom Automation Engine
Solution Accelerator - Multi Agent Custom Automation Engine
alguadam
Alvaro Guadamillas Herranz
63avm/ptn/security/security-centerAzure Security Center (Defender for Cloud)oZakari
Zach Trocinski
jtracey93
Jack Tracey
64avm/ptn/security/sentinelSentinel
65avm/ptn/subscription/service-health-alertsService Health Alertssebassem
Seif Bassem
66avm/ptn/virtual-machine-images/azure-image-builderCustom Images using Azure Image BuilderAlexanderSehr
Alexander Sehr

Module Publication History - πŸ“…

βž• Module Publication History - Module names, status and owners

Modules published in April 2026

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/network/virtual-wanVirtual WAN
vWAN
ericscheffler
Eric Scheffler
juancj
Juan Jimenez

Modules published in September 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/sa/build-your-own-copilotSA - Build your own Copilot
Solution Accelerator - Build your own Copilot (BYOC)

02avm/ptn/sa/chat-with-your-dataSA - Chat with your data
Solution Accelerator - Chat with your data (CWYD)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz
03avm/ptn/sa/document-knowledge-miningSA - Document knowledge mining
Solution Accelerator - Document knowledge mining (DKM)
aniaroramsft
Anish Arora
alguadam
Alvaro Guadamillas Herranz

Modules published in August 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/alz/amaAzure Landing Zones (ALZ) - Azure Monitoring Agent (AMA)oZakari
Zach Trocinski

Modules published in July 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/ai-ml/ai-foundryAI-ML - AI Foundrymswantek68
Mike Swantek
sethsteenken
Seth Steenken
02avm/ptn/app/iaas-vm-cosmosdb-tier4Workload - IaaS VM Cosmos DB - Tier 4mikestiers
Mike Stiers
03avm/ptn/sa/content-processingSA - Content Processing
Solution Accelerator - Content Processing
brittneek
Brittnee Keller
04avm/ptn/sa/modernize-your-codeSA - Modernize your code
Solution Accelerator - Modernize your code
sethsteenken
Seth Steenken

Modules published in June 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/sa/multi-agent-custom-automation-engineSA - Multi Agent Custom Automation Engine
Solution Accelerator - Multi Agent Custom Automation Engine
alguadam
Alvaro Guadamillas Herranz
02avm/ptn/subscription/service-health-alertsService Health Alertssebassem
Seif Bassem

Modules published in April 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/alz/emptyAzure Landing Zones (ALZ) - Emptyjtracey93
Jack Tracey
oZakari
Zach Trocinski
02avm/ptn/app-service-lza/hosting-environmentApp Service LZA - Hosting EnvironmentMikeTB-Microsoft
Michael Baker

Modules published in March 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/sa/conversation-knowledge-miningSA - Conversation knowledge mining
Solution Accelerator - Conversation knowledge mining (CKM)
alguadam
Alvaro Guadamillas Herranz

Modules published in February 2025

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/authorization/pim-role-assignmentAuthorization - PIM Role Assignmentsebassem
Seif Bassem
02avm/ptn/mgmt-groups/subscription-placementManagement Groups - Subscription PlacementoZakari
Zach Trocinski

Modules published in December 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/app/container-job-toolkitContainer Job ToolkitReneHezser
Rene Hezser
02avm/ptn/authorization/policy-exemptionAuthorization - Policy ExemptionoZakari
Zach Trocinski
03avm/ptn/authorization/role-definitionAuthorization - Role Definitionjtracey93
Jack Tracey
04avm/ptn/azd/aks-automatic-clusterAZD - AKS Automatic Cluster
Azure Developer CLI - Azure Kubernetes Services Automatic Cluster
JeffreyCA
Jeffrey Chen

Modules published in October 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/azd/acr-container-appAZD - ACR Container App
Azure Developer CLI - ACR Container App
JeffreyCA
Jeffrey Chen
02avm/ptn/azd/aksAZD - AKS
Azure Developer CLI - Azure Kubernetes Services
JeffreyCA
Jeffrey Chen
03avm/ptn/azd/container-app-upsertAZD - Container App Upsert
Azure Developer CLI - Container Apps Upsert
JeffreyCA
Jeffrey Chen
04avm/ptn/azd/container-apps-stackAZD - Container Apps Stack
Azure Developer CLI - Container Apps Stack
JeffreyCA
Jeffrey Chen
05avm/ptn/azd/ml-ai-environmentAZD - ML AI Environment
Azure Developer CLI - Machine Learning AI Environment

06avm/ptn/azd/ml-hub-dependenciesAZD - ML Hub Dependencies
Azure Developer CLI - Machine Learning Hub Dependencies

07avm/ptn/azd/ml-projectAZD - ML Project
Azure Developer CLI - Machine Learning Project

08avm/ptn/azd/monitoringAZD - Monitoring
Azure Developer CLI - Monitoring
JeffreyCA
Jeffrey Chen
09avm/ptn/data/private-analytical-workspacePrivate Analytical Workspace
Data Analytics, Data Lake, Databricks, Database
jbinko
Jiri Binko

Modules published in September 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/azd/apim-apiAZD - APIM API
Azure Developer CLI - API Management
JeffreyCA
Jeffrey Chen
02avm/ptn/azd/insights-dashboardAZD - Insights Dashboard
Azure Developer CLI - Inishgts Dashboard
JeffreyCA
Jeffrey Chen
03avm/ptn/dev-ops/cicd-agents-and-runnersAzure DevOps and GitHub CI/CD Agents and Runnerssebassem
Seif Bassem
04avm/ptn/network/hub-networkingHub NetworkingoZakari
Zach Trocinski
jtracey93
Jack Tracey
05avm/ptn/virtual-machine-images/azure-image-builderCustom Images using Azure Image BuilderAlexanderSehr
Alexander Sehr

Modules published in August 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/aca-lza/hosting-environmentAzure Container Apps (ACA) LZA - Hosting Environmentkpantos
Konstantinos Pantos

Modules published in July 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/ai-platform/baselineAI Platform - Baselinececheta
Chinedum Echeta
ross-p-smith
Ross Smith
02avm/ptn/deployment-script/import-image-to-acrDeployment Script - Import Container Image to ACRReneHezser
Rene Hezser
03avm/ptn/network/private-link-private-dns-zonesPrivate Link Private DNS Zonesjtracey93
Jack Tracey

Modules published in June 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/lz/sub-vendingLanding Zone Subscription Vendingjtracey93
Jack Tracey
sebassem
Seif Bassem

Modules published in May 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/authorization/resource-role-assignmentAuthorization - Resource Role Assignmentpeterbud
Peter Budai
02avm/ptn/finops-toolkit/finops-hubFinOps Toolkit - FinOps HubFallenHoot
Zach Olinske

Modules published in April 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/ptn/authorization/policy-assignmentAuthorization - Policy Assignmentjtracey93
Jack Tracey
02avm/ptn/authorization/role-assignmentAuthorization - Role Assignmentjtracey93
Jack Tracey
03avm/ptn/policy-insights/remediationPolicy Insights Remediationdonk-msft
Don Koning
04avm/ptn/security/security-centerAzure Security Center (Defender for Cloud)oZakari
Zach Trocinski
jtracey93
Jack Tracey

For Module Owners & Contributors

Note

This section is mainly intended for module owners and contributors as it contains information important for module development, such as telemetry ID prefix, and GitHub Teams for Owners.

Module name, Telemetry ID prefix, GitHub Teams for Owners

βž• All Modules - Module name, Telemetry ID prefix, GitHub Teams for Owners
No.Module NameTelemetry ID prefixGitHub Teams for Module Owners (@Azure org)
01avm/ptn/aca-lza/hosting-environment46d3xbcp.ptn.acalza-hostingenvironmentavm-ptn-acalza-hostingenvironment-module-owners-bicep
02avm/ptn/ai-ml/ai-foundry46d3xbcp.ptn.aiml-aifoundryavm-ptn-aiml-aifoundry-module-owners-bicep
03avm/ptn/ai-ml/landing-zone46d3xbcp.ptn.aiml-lzavm-ptn-aiml-landingzone-module-owners-bicep
04avm/ptn/ai-platform/baseline46d3xbcp.ptn.aiplatform-baselineavm-ptn-aiplatform-baseline-module-owners-bicep
05avm/ptn/alz/ama46d3xbcp.ptn.alz-amaavm-ptn-alz-ama-module-owners-bicep
06avm/ptn/alz/empty46d3xbcp.ptn.alz-emptyavm-ptn-alz-empty-module-owners-bicep
07avm/ptn/app-service-lza/hosting-environment46d3xbcp.ptn.appsvclza-hostingenvironmentavm-ptn-appservicelza-hostingenvironment-module-owners-bicep
08avm/ptn/app/container-job-toolkit46d3xbcp.ptn.app-containerjobtoolkitavm-ptn-app-containerjobtoolkit-module-owners-bicep
09avm/ptn/app/cosmos-db-account-container-app46d3xbcp.ptn.app-cosmosdbacctcontappavm-ptn-app-cosmosdbaccountcontainerapp-module-owners-bicep
10avm/ptn/app/iaas-vm-cosmosdb-tier446d3xbcp.ptn.app-iaasvmcosmosdbt4avm-ptn-app-iaasvmcosmosdbtier4-module-owners-bicep
11avm/ptn/app/mongodb-cluster-container-app46d3xbcp.ptn.app-mongodbclustcontappavm-ptn-app-mongodbclustercontainerapp-module-owners-bicep
12avm/ptn/app/paas-ase-cosmosdb-tier446d3xbcp.ptn.app-paasasecosmosdbt4avm-ptn-app-paasasecosmosdbtier4-module-owners-bicep
13avm/ptn/authorization/pim-role-assignment46d3xbcp.ptn.authorization-pimroleassignmentavm-ptn-authorization-pimroleassignment-module-owners-bicep
14avm/ptn/authorization/policy-assignment46d3xbcp.ptn.authorization-policyassignmentavm-ptn-authorization-policyassignment-module-owners-bicep
15avm/ptn/authorization/policy-exemption46d3xbcp.ptn.authorization-policyexemptionavm-ptn-authorization-policyexemption-module-owners-bicep
16avm/ptn/authorization/resource-role-assignment46d3xbcp.ptn.authorization-resourceroleassignmentavm-ptn-authorization-resourceroleassignment-module-owners-bicep
17avm/ptn/authorization/role-assignment46d3xbcp.ptn.authorization-roleassignmentavm-ptn-authorization-roleassignment-module-owners-bicep
18avm/ptn/authorization/role-definition46d3xbcp.ptn.authorization-roledefinitionavm-ptn-authorization-roledefinition-module-owners-bicep
19avm/ptn/avd-lza/insights46d3xbcp.ptn.avdlza-insightsavm-ptn-avdlza-insights-module-owners-bicep
20avm/ptn/avd-lza/management-plane46d3xbcp.ptn.avdlza-managementplaneavm-ptn-avdlza-managementplane-module-owners-bicep
21avm/ptn/avd-lza/networking46d3xbcp.ptn.avdlza-networkingavm-ptn-avdlza-networking-module-owners-bicep
22avm/ptn/avd-lza/session-hosts46d3xbcp.ptn.avdlza-sessionhostsavm-ptn-avdlza-sessionhosts-module-owners-bicep
23avm/ptn/azd/acr-container-app46d3xbcp.ptn.azd-acrcontainerappavm-ptn-azd-acrcontainerapp-module-owners-bicep
24avm/ptn/azd/aks46d3xbcp.ptn.azd-aksavm-ptn-azd-aks-module-owners-bicep
25avm/ptn/azd/aks-automatic-cluster46d3xbcp.ptn.azd-aksautomaticclusteravm-ptn-azd-aksautomaticcluster-module-owners-bicep
26avm/ptn/azd/apim-api46d3xbcp.ptn.azd-apimapiavm-ptn-azd-apimapi-module-owners-bicep
27avm/ptn/azd/container-app-upsert46d3xbcp.ptn.azd-containerappupsertavm-ptn-azd-containerappupsert-module-owners-bicep
28avm/ptn/azd/container-apps-stack46d3xbcp.ptn.azd-containerappsstackavm-ptn-azd-containerappsstack-module-owners-bicep
29avm/ptn/azd/insights-dashboard46d3xbcp.ptn.azd-insightsdashboardavm-ptn-azd-insightsdashboard-module-owners-bicep
30avm/ptn/azd/ml-ai-environment46d3xbcp.ptn.azd-mlaienvironmentavm-ptn-azd-mlaienvironment-module-owners-bicep
31avm/ptn/azd/ml-hub-dependencies46d3xbcp.ptn.azd-mlhubdependenciesavm-ptn-azd-mlhubdependencies-module-owners-bicep
32avm/ptn/azd/ml-project46d3xbcp.ptn.azd-mlprojectavm-ptn-azd-mlproject-module-owners-bicep
33avm/ptn/azd/monitoring46d3xbcp.ptn.azd-monitoringavm-ptn-azd-monitoring-module-owners-bicep
34avm/ptn/data/private-analytical-workspace46d3xbcp.ptn.data-privateanalyticalworkspaceavm-ptn-data-privateanalyticalworkspace-module-owners-bicep
35avm/ptn/deployment-script/create-kv-ssh-key-pair46d3xbcp.ptn.deploymentscript-createkvsshkeypairavm-ptn-deploymentscript-createkvsshkeypair-module-owners-bicep
36avm/ptn/deployment-script/import-image-to-acr46d3xbcp.ptn.deploymentscript-importimagetoacravm-ptn-deploymentscript-importimagetoacr-module-owners-bicep
37avm/ptn/deployment-script/private46d3xbcp.ptn.deploymentscript-privateavm-ptn-deploymentscript-private-module-owners-bicep
38avm/ptn/dev-center/dev-box46d3xbcp.ptn.devcenter-devboxavm-ptn-devcenter-devbox-module-owners-bicep
39avm/ptn/dev-ops/cicd-agents-and-runners46d3xbcp.ptn.devops-cicdagentsandrunnersavm-ptn-devops-cicdagentsandrunners-module-owners-bicep
40avm/ptn/finops-toolkit/finops-hub46d3xbcp.ptn.finopstoolkit-finopshubavm-ptn-finopstoolkit-finopshub-module-owners-bicep
41avm/ptn/lz/sub-vending46d3xbcp.ptn.lz-subvendingavm-ptn-lz-subvending-module-owners-bicep
42avm/ptn/lza-shared/data-services46d3xbcp.ptn.lzashared-dataservicesavm-ptn-lzashared-dataservices-module-owners-bicep
43avm/ptn/maintenance/azure-update-manager46d3xbcp.ptn.maintenance-azureupdatemanageravm-ptn-maintenance-azureupdatemanager-module-owners-bicep
44avm/ptn/mgmt-groups/subscription-placement46d3xbcp.ptn.mgmtgroup-subplacementavm-ptn-mgmtgroups-subscriptionplacement-module-owners-bicep
45avm/ptn/monitoring/amba46d3xbcp.ptn.monitoring-ambaavm-ptn-monitoring-amba-module-owners-bicep
46avm/ptn/monitoring/amba-alz46d3xbcp.ptn.monitoring-ambaalzavm-ptn-monitoring-ambaalz-module-owners-bicep
47avm/ptn/network/hub-networking46d3xbcp.ptn.network-hubnetworkingavm-ptn-network-hubnetworking-module-owners-bicep
48avm/ptn/network/private-link-private-dns-zones46d3xbcp.ptn.network-privatelinkprivatednszonesavm-ptn-network-privatelinkprivatednszones-module-owners-bicep
49avm/ptn/network/virtual-wan46d3xbcp.ptn.network-virtualwanavm-ptn-network-virtualwan-module-owners-bicep
50avm/ptn/network/vwan-connected-vnets46d3xbcp.ptn.network-vwanconnectedvnetsavm-ptn-network-vwanconnectedvnets-module-owners-bicep
51avm/ptn/openai/cognitive-search46d3xbcp.ptn.openai-cognitivesearchavm-ptn-openai-cognitivesearch-module-owners-bicep
52avm/ptn/openai/e2e-baseline46d3xbcp.ptn.openai-e2ebaselineavm-ptn-openai-e2ebaseline-module-owners-bicep
53avm/ptn/policy-insights/remediation46d3xbcp.ptn.policyinsights-remediationavm-ptn-policyinsights-remediation-module-owners-bicep
54avm/ptn/sa/build-your-own-copilot46d3xbcp.ptn.sa-buildyourowncopilotavm-ptn-sa-buildyourowncopilot-module-owners-bicep
55avm/ptn/sa/chat-with-your-data46d3xbcp.ptn.sa-chatwithyourdataavm-ptn-sa-chatwithyourdata-module-owners-bicep
56avm/ptn/sa/content-generation46d3xbcp.ptn.sa-contentgenerationavm-ptn-sa-contentgeneration-module-owners-bicep
57avm/ptn/sa/content-processing46d3xbcp.ptn.sa-contentprocessingavm-ptn-sa-contentprocessing-module-owners-bicep
58avm/ptn/sa/conversation-knowledge-mining46d3xbcp.ptn.sa-convknowledgeminingavm-ptn-sa-conversationknowledgemining-module-owners-bicep
59avm/ptn/sa/customer-chatbot46d3xbcp.ptn.sa-customerchatbotavm-ptn-sa-customerchatbot-module-owners-bicep
60avm/ptn/sa/document-knowledge-mining46d3xbcp.ptn.sa-documentknowledgeminingavm-ptn-sa-documentknowledgemining-module-owners-bicep
61avm/ptn/sa/modernize-your-code46d3xbcp.ptn.sa-modernizeyourcodeavm-ptn-sa-modernizeyourcode-module-owners-bicep
62avm/ptn/sa/multi-agent-custom-automation-engine46d3xbcp.ptn.sa-multiagentcustautengavm-ptn-sa-multiagentcustomautomationengine-module-owners-bicep
63avm/ptn/security/security-center46d3xbcp.ptn.security-securitycenteravm-ptn-security-securitycenter-module-owners-bicep
64avm/ptn/security/sentinel46d3xbcp.ptn.security-sentinelavm-ptn-security-sentinel-module-owners-bicep
65avm/ptn/subscription/service-health-alerts46d3xbcp.ptn.subscription-svchealthalertsavm-ptn-subscription-servicehealthalerts-module-owners-bicep
66avm/ptn/virtual-machine-images/azure-image-builder46d3xbcp.ptn.vmimages-azureimagebuilderavm-ptn-virtualmachineimages-azureimagebuilder-module-owners-bicep

Bicep Utility Modules

Module catalog

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
BicepUtility112
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Info

This page contains various views of the module index (catalog) for Bicep Utility Modules. To see these views, click on the expandable sections with the “βž•” sign below.

  • To see the full, unfiltered, unformatted module index on GitHub, click here.

  • To download the source CSV file, click here.

Note

Modules listed below that aren’t shown with the status of Module Available 🟒, are currently in development and are not yet available for use. For proposed modules, see the Proposed modules section below.

Published modules - 🟒 & 🟑

βž• Published Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/utl/types/avm-common-typesAVM Common TypesAlexanderSehr
Alexander Sehr

Proposed modules - βšͺ

βž• Proposed Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/utl/general/get-environmentGet-Environmentalex-frankel
Alex Frankel

Deprecated modules - πŸ”΄

βž• Deprecated Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01❌ None listed❌ None listed❌ None listed❌ None listed

All modules - πŸ“‡

βž• All Modules - Module names, status and owners
No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/utl/general/get-environmentGet-Environmentalex-frankel
Alex Frankel
02avm/utl/types/avm-common-typesAVM Common TypesAlexanderSehr
Alexander Sehr

Module Publication History - πŸ“…

βž• Module Publication History - Module names, status and owners

Modules published in October 2024

No.Module NameDisplay NameStatus & VersionsOwner(s)
01avm/utl/types/avm-common-typesAVM Common TypesAlexanderSehr
Alexander Sehr

For Module Owners & Contributors

Note

This section is mainly intended for module owners and contributors as it contains information important for module development, such as telemetry ID prefix, and GitHub Teams for Owners.

Module name, Telemetry ID prefix, GitHub Teams for Owners

βž• All Modules - Module name, Telemetry ID prefix, GitHub Teams for Owners
No.Module NameTelemetry ID prefixGitHub Teams for Module Owners (@Azure org)
01avm/utl/general/get-environment46d3xbcp.utl.general-getenvironmentavm-utl-general-getenvironment-module-owners-bicep
02avm/utl/types/avm-common-types46d3xbcp.utl.types-avmcommontypesavm-utl-types-avmcommontypes-module-owners-bicep

Terraform Modules

Summary

The following table shows the number of all available, orphaned and planned Terraform Modules.

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
TerraformResource11042152
Pattern252651
Utility11314
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Want to contribute to AVM Terraform modules?
#LabelsLink and description
1.Type: New Module Proposal πŸ’‘
Needs: Module Owner πŸ“£
Language: Terraform 🌐
To become the owner of a new Terraform module, see all new Terraform modules looking for owners or check out the “Looking for owners” swimlane here.
2.Status: Module Orphaned 🟑
Language: Terraform 🌐
To become the owner of an orphaned Terraform module, see all orphaned Terraform modules or check out the “Orphaned” swimlane here.
3.Needs: Module Contributor πŸ“£ Language: Terraform 🌐To become a co-owner or contribute to a Terraform module, see all Terraform modules looking for contributors.

For more details on “What are the different ways to contribute to AVM?”, see here.

Subsections of Terraform

Terraform Resource Modules

Module catalog

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
TerraformResource11042152
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Info

This page contains various views of the module index (catalog) for Terraform Resource Modules. To see these views, click on the expandable sections with the “βž•” sign below.

  • To see the full, unfiltered, unformatted module index on GitHub, click here.

  • To download the source CSV file, click here.

Note

Modules listed below that aren’t shown with the status of Module Available 🟒, are currently in development and are not yet available for use. For proposed modules, see the Proposed modules section below.

Published modules - 🟒 & 🟑

βž• Published Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-res-apimanagement-serviceπŸ“„API Management Serviceswatilekhapaul
Swatilekha Paul
02avm-res-app-containerappπŸ“„Container Applonegunmanb
Zijie He
03avm-res-app-jobπŸ“„App Jobsujaypillai
Sujay Pillai
04avm-res-app-managedenvironmentπŸ“„App Managed Environmentsegraef
Sebastian Graef
05avm-res-appconfiguration-configurationstoreπŸ“„App Configuration Storematt-FFFFFF
Matt White
06avm-res-authorization-roleassignmentπŸ“„Role Assignmentjaredfholgate
Jared Holgate
07avm-res-automation-automationaccountπŸ“„Automation Accountdidayal-msft
Divyadeep Dayal
Poven795909
Poornima Venkataramanan
08avm-res-avs-privatecloudπŸ“„AVS Private Cloudjchancellor-ms
Jon Chancellor
09avm-res-azurestackhci-clusterπŸ“„Azure Stack HCI Clusterchirag1603
Chirag Choudha
10avm-res-azurestackhci-logicalnetworkπŸ“„AzureStackHCI logical networkchirag1603
Chirag Choudha
11avm-res-azurestackhci-virtualmachineinstanceπŸ“„Stack HCI Virtual Machine Instancechirag1603
Chirag Choudha
12avm-res-batch-batchaccountπŸ“„Batch Accountethanjenkins1
Ethan Jenkins
13avm-res-botservice-botserviceπŸ“„Bot Serviceethanjenkins1
Ethan Jenkins
14avm-res-cache-redisπŸ“„Redis Cachejchancellor-ms
Jon Chancellor
15avm-res-cache-redisenterpriseπŸ“„Azure Managed RedisAnkur1106
Ankur Sharma
16avm-res-cdn-profileπŸ“„CDN ProfilePoven795909
Poornima Venkataramanan
didayal-msft
Divyadeep Dayal
17avm-res-certificateregistration-certificateorderπŸ“„Certificate Orderslonegunmanb
Zijie He
18avm-res-cognitiveservices-accountπŸ“„Cognitive Servicelonegunmanb
Zijie He
19avm-res-communication-emailserviceπŸ“„Email Communication Servicelonegunmanb
Zijie He
20avm-res-compute-capacityreservationgroupπŸ“„Capacity Reservation GroupWenAI2020
Wen Tian
21avm-res-compute-diskπŸ“„Compute Diskterrymandin
Terry Mandin
22avm-res-compute-diskencryptionsetπŸ“„Disk Encryption SetAkashc0807
Akash Choudhary
23avm-res-compute-galleryπŸ“„Azure Compute GalleryAkashc0807
Akash Choudhary
24avm-res-compute-hostgroupπŸ“„Host GroupsWenAI2020
Wen Tian
25avm-res-compute-proximityplacementgroupπŸ“„Proximity Placement Groupfafriha
Farouk Friha
26avm-res-compute-sshpublickeyπŸ“„Public SSH KeyChrisSidebotham
Chris Sidebotham
27avm-res-compute-virtualmachineπŸ“„Virtual Machine
VM
jchancellor-ms
Jon Chancellor
28avm-res-compute-virtualmachinescalesetπŸ“„Virtual Machine Scale Set
VMSS
terrymandin
Terry Mandin
marcelkmfst
Marcel Keller
29avm-res-containerinstance-containergroupπŸ“„Container Instancesharmilamusunuru
Sharmila Musunuru
30avm-res-containerregistry-registryπŸ“„Azure Container Registry (ACR)Akashc0807
Akash Choudhary
31avm-res-containerservice-managedclusterπŸ“„AKS Managed ClusteribersanoMS
Isabelle Bersano
32avm-res-databricks-workspaceπŸ“„Azure Databricks Workspacesegraef
Sebastian Graef
33avm-res-datafactory-factoryπŸ“„Data Factoryasishr
Asish R
34avm-res-dataprotection-backupvaultπŸ“„Data Protection Backup Vaultethanjenkins1
Ethan Jenkins
35avm-res-dataprotection-resourceguardπŸ“„Data Protection Resource GuardWenAI2020
Wen Tian
36avm-res-dbformysql-flexibleserverπŸ“„DB for MySQL Flexible Serverelsalvos
Cesar Abrego
37avm-res-dbforpostgresql-flexibleserverπŸ“„DB for Postgre SQL Flexible Serverzaidmohd
Zaid Mohammad
38avm-res-desktopvirtualization-applicationgroupπŸ“„Azure Virtual Desktop (AVD) Application Groupjensheerin
Jen Sheerin
sihbher
Gerardo Reyes
39avm-res-desktopvirtualization-hostpoolπŸ“„Azure Virtual Desktop (AVD) Host Pooljensheerin
Jen Sheerin
sihbher
Gerardo Reyes
40avm-res-desktopvirtualization-scalingplanπŸ“„Azure Virtual Desktop (AVD) Scaling Planjensheerin
Jen Sheerin
sihbher
Gerardo Reyes
41avm-res-desktopvirtualization-workspaceπŸ“„Azure Virtual Desktop (AVD) Workspacejensheerin
Jen Sheerin
sihbher
Gerardo Reyes
42avm-res-devcenter-devcenterπŸ“„Dev Centerabhishekaryams
Abhishek Arya
43avm-res-devopsinfrastructure-poolπŸ“„DevOps Poolsjaredfholgate
Jared Holgate
44avm-res-documentdb-databaseaccountπŸ“„CosmosDB Database Accountcmaneu
Christopher Maneu
45avm-res-documentdb-mongoclusterπŸ“„Cosmos DB for MongoDB (vCore)sujaypillai
Sujay Pillai
46avm-res-edge-siteπŸ“„Azure Arc Site managerchirag1603
Chirag Choudha
47avm-res-eventgrid-domainπŸ“„Event Grid Domainsujaypillai
Sujay Pillai
48avm-res-eventgrid-namespaceπŸ“„Event Grid Namespacedassbernd
Berna Sandalli
49avm-res-eventgrid-topicπŸ“„Event Grid Topicsujaypillai
Sujay Pillai
50avm-res-eventhub-namespaceπŸ“„Event Hub Namespacercavaturu
Raja Kalyan Ram Cavaturu
51avm-res-features-featureπŸ“„Azure Feature Exposure Control (AFEC)lonegunmanb
Zijie He
52avm-res-hybridcontainerservice-provisionedclusterinstanceπŸ“„AKS Arcchirag1603
Chirag Choudha
53avm-res-insights-autoscalesettingπŸ“„Auto scale settingsMinHeinA
Min Hein Aung
54avm-res-insights-componentπŸ“„Application InsightJfolberth
John Folberth
55avm-res-insights-datacollectionendpointπŸ“„Data Collection Endpointsharmilamusunuru
Sharmila Musunuru
56avm-res-keyvault-vaultπŸ“„Key Vault
KV
matt-FFFFFF
Matt White
57avm-res-kusto-clusterπŸ“„Kusto ClustersLaurentLesle
Laurent Lesle
58avm-res-logic-workflowπŸ“„Logic Apps (Workflow)bakrish
Bala Krishnamoorthy
59avm-res-machinelearningservices-workspaceπŸ“„Machine Learning Services Workspace
ML Workspace
Nepomuceno
Gabriel Monteiro Nepomuceno
60avm-res-maintenance-maintenanceconfigurationπŸ“„Maintenance ConfigurationASHR4
Rhys Ash
61avm-res-managedidentity-userassignedidentityπŸ“„User Assigned Identity
MSI
Jfolberth
John Folberth
62avm-res-management-servicegroupπŸ“„Management Service Groupshaflidif
Haflidi Fridthjofsson
63avm-res-netapp-netappaccountπŸ“„Azure NetApp Filejtracey93
Jack Tracey
64avm-res-network-applicationgatewayπŸ“„Application Gateway
App GW
mofaizal
Mohamed Faizal
65avm-res-network-applicationgatewaywebapplicationfirewallpolicyπŸ“„Application Gateway Web Application Firewall (WAF) Policymofaizal
Mohamed Faizal
66avm-res-network-applicationsecuritygroupπŸ“„Application Security Group (ASG)
ASG
MinHeinA
Min Hein Aung
67avm-res-network-azurefirewallπŸ“„Azure Firewall
Azure FW
vmisson
Vincent Misson
68avm-res-network-bastionhostπŸ“„Bastion Hosthumanascode
Itamar Hirosh
69avm-res-network-connectionπŸ“„Virtual Network Gateway Connectionjchancellor-ms
Jon Chancellor
70avm-res-network-ddosprotectionplanπŸ“„DDoS Protectionsitarant
Simona Tarantola
jtracey93
Jack Tracey
71avm-res-network-dnsresolverπŸ“„DNS Resolverhumanascode
Itamar Hirosh
72avm-res-network-dnszoneπŸ“„Public DNS Zonesharmilamusunuru
Sharmila Musunuru
73avm-res-network-expressroutecircuitπŸ“„ExpressRoute Circuit
ER
khushal08
Khush Kaviraj
adammontlake
Adam Montlake
74avm-res-network-firewallpolicyπŸ“„Azure Firewall PolicyMinHeinA
Min Hein Aung
75avm-res-network-frontdoorwebapplicationfirewallpolicyπŸ“„Front Door Web Application Firewall (WAF) Policysihbher
Gerardo Reyes
76avm-res-network-ipgroupπŸ“„IP Groupmathewsg
Mathews George
77avm-res-network-loadbalancerπŸ“„Loadbalancerdonovm4
Donovan McCoy
78avm-res-network-localnetworkgatewayπŸ“„Local Network GatewayBhavyasree08
Bhavyasree Damarla
79avm-res-network-natgatewayπŸ“„NAT Gatewayiarik0440
Yarik Simineans
80avm-res-network-networkinterfaceπŸ“„Network Interface
NIC
fafriha
Farouk Friha
81avm-res-network-networkmanagerπŸ“„Azure Virtual Network ManagerMrRoundRobin
Robin Muller
82avm-res-network-networksecuritygroupπŸ“„Network Security Groupmaheshbenke
Mahesh Benke
83avm-res-network-networkwatcherπŸ“„Azure Network Watcherterrymandin
Terry Mandin
84avm-res-network-privatednszoneπŸ“„Private DNS ZoneMinHeinA
Min Hein Aung
85avm-res-network-privateendpointπŸ“„Private Endpointjaredfholgate
Jared Holgate
86avm-res-network-publicipaddressπŸ“„Public IP Address
PIP
vmisson
Vincent Misson
87avm-res-network-publicipprefixπŸ“„Public IP PrefixPmeshramPM
Pankaj Meshram
88avm-res-network-routetableπŸ“„Route Table
UDR
adammontlake
Adam Montlake
89avm-res-network-trafficmanagerprofileπŸ“„Traffic Manager ProfileAnubhaR94
Anubha Rana
90avm-res-network-virtualnetworkπŸ“„Virtual Network
VNET
jaredfholgate
Jared Holgate
91avm-res-operationalinsights-workspaceπŸ“„Log Analytics workspaceiarik0440
Yarik Simineans
92avm-res-oracledatabase-cloudexadatainfrastructureπŸ“„Oracle Exadata Infrastructuresihbher
Gerardo Reyes
terrymandin
Terry Mandin
93avm-res-oracledatabase-cloudvmclusterπŸ“„Oracle VM clustersihbher
Gerardo Reyes
terrymandin
Terry Mandin
94avm-res-portal-dashboardπŸ“„Azure Portal DashboardVeronicaSea
Veronica Xu
95avm-res-recoveryservices-vaultπŸ“„Recovery Services Vaultelsalvos
Cesar Abrego
96avm-res-redhatopenshift-openshiftclusterπŸ“„OpenShift Clusterethanjenkins1
Ethan Jenkins
puneetdevadiga
Puneet Devadiga
97avm-res-relay-namespaceπŸ“„Relay Namespacesujaypillai
Sujay Pillai
98avm-res-resourcegraph-queryπŸ“„Resource Graph QuerySJAYAP
S Jayaprakash
99avm-res-resources-resourcegroupπŸ“„Resource Group
RG
Jfolberth
John Folberth
100avm-res-search-searchserviceπŸ“„Search Servicelestermarch
Lester March
101avm-res-servicebus-namespaceπŸ“„Service Bus Namespaceiarik0440
Yarik Simineans
102avm-res-sql-managedinstanceπŸ“„SQL Managed Instance
SQL MI
chanakanissanka
Chanaka Nissanka
abhishekaryams
Abhishek Arya
103avm-res-sql-serverπŸ“„Azure SQL Serverabhishekaryams
Abhishek Arya
104avm-res-sqlvirtualmachine-sqlvirtualmachineπŸ“„Sql Virtual Machine
SQL VM
sujaypillai
Sujay Pillai
105avm-res-storage-storageaccountπŸ“„Storage Accountchinthakaru
Chinthaka Rupasinghe
106avm-res-web-connectionπŸ“„API Connectiondonovm4
Donovan McCoy
107avm-res-web-hostingenvironmentπŸ“„App Service Environment
ASE
ibersanoMS
Isabelle Bersano
108avm-res-web-serverfarmπŸ“„App Service PlanibersanoMS
Isabelle Bersano
109avm-res-web-siteπŸ“„Web/Function App
App Service, Web Site, Logic App, Function App
donovm4
Donovan McCoy
110avm-res-web-staticsiteπŸ“„Static Web Appdonovm4
Donovan McCoy

Proposed modules - βšͺ

βž• Proposed Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-res-aad-domainservicen/aAzure Active Directory Domain Servicehumanascode
Itamar Hirosh
02avm-res-alertsmanagement-actionrulen/aAction Rulesxsatishx
Satish Balakrishnan
03avm-res-analysisservices-servern/aAnalysis Services ServerBhavyasree08
Bhavyasree Damarla
04avm-res-compute-imagen/aImage
05avm-res-consumption-budgetn/aConsumption BudgetAkashc0807
Akash Choudhary
06avm-res-containerservice-fleetn/aAKS Fleetdidayal-msft
Divyadeep Dayal
amruta53
Amruta Kulkarni
07avm-res-dashboard-grafanan/aAzure Managed Grafanakhajour
Abdelaziz Khajour
08avm-res-databricks-accessconnectorn/aAzure Databricks Access Connector
09avm-res-deviceregistry-assetendpointprofilen/aDevice Registry Asset Endpoint Profilelonegunmanb
Zijie He
10avm-res-devtestlab-labn/aDevTest Labsharmilamusunuru
Sharmila Musunuru
11avm-res-digitaltwins-digitaltwinsinstancen/aDigital Twins Instance
12avm-res-eventgrid-systemtopicn/aEvent Grid System Topicsujaypillai
Sujay Pillai
13avm-res-healthbot-healthbotn/aAzure Health Bot
14avm-res-hybridcompute-machinen/aHybrid Compute Machine
15avm-res-insights-actiongroupn/aAction Grouparjenhuitema
Arjen Huitema
Brunoga-MS
Bruno Gabrielli
16avm-res-insights-activitylogalertn/aActivity log alertstagolovina
Tanya Golovina
17avm-res-insights-datacollectionrulen/aData Collection Rulesharmilamusunuru
Sharmila Musunuru
18avm-res-insights-logprofilen/aLog profilesharmilamusunuru
Sharmila Musunuru
19avm-res-insights-metricalertn/aMetric Alertarjenhuitema
Arjen Huitema
20avm-res-insights-privatelinkscopen/aAzure Monitor Private Link Scopesharmilamusunuru
Sharmila Musunuru
21avm-res-insights-scheduledqueryrulen/aScheduled Query Rulexsatishx
Satish Balakrishnan
22avm-res-iotoperations-instancen/aAzure IoT Operationsagreaves-ms
Allen Greaves
WilliamBerryiii
Bill Berry
23avm-res-loadtestservice-loadtestn/aLoad Testing Servicepfayika1
Philippe Fayika
24avm-res-managedservices-registrationdefinitionn/aRegistration Definition (Lighthouse)
25avm-res-management-managementgroupn/aManagement Group
MG
herms14
Hermes Miraflor II
26avm-res-network-dnsforwardingrulesetn/aDNS Forwarding Rulesetsharmilamusunuru
Sharmila Musunuru
27avm-res-network-frontdoorn/aAzure Front Door
28avm-res-network-networksecurityperimetern/aNetwork Security Perimetersujaypillai
Sujay Pillai
29avm-res-network-privatelinkservicen/aPrivate Link Serviceavivshrem
Aviv Shrem
30avm-res-network-serviceendpointpolicyn/aService Endpoint PolicyASHR4
Rhys Ash
31avm-res-network-virtualnetworkgatewayn/aVirtual Network Gateway
VNET GW

32avm-res-network-virtualroutern/aRoute Server
33avm-res-operationsmanagement-solutionn/aOperations Management Solution
34avm-res-powerbidedicated-capacityn/aPower BI Dedicated Capacity
35avm-res-purview-accountn/aPurview Account
36avm-res-resources-featuren/aResource Featureslonegunmanb
Zijie He
37avm-res-servicefabric-clustern/aService Fabric Cluster
38avm-res-servicenetworking-trafficcontrollern/aApplication Gateway for Containers (Traffic Controller)mofaizal
Mohamed Faizal
39avm-res-signalrservice-signalrn/aSignalR Service SignalR
40avm-res-sql-instancepooln/aInstance Poolssujaypillai
Sujay Pillai
41avm-res-synapse-workspacen/aSynapse WorkspaceKarni-G
Karni Gupta
42avm-res-virtualmachineimages-imagetemplaten/aVirtual Machine Image Templatetravishankins
Travis Hankins

Deprecated modules - πŸ”΄

βž• Deprecated Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01❌ None listed❌ None listed❌ None listed❌ None listed❌ None listed

All modules - πŸ“‡

βž• All Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-res-aad-domainservicen/aAzure Active Directory Domain Servicehumanascode
Itamar Hirosh
02avm-res-alertsmanagement-actionrulen/aAction Rulesxsatishx
Satish Balakrishnan
03avm-res-analysisservices-servern/aAnalysis Services ServerBhavyasree08
Bhavyasree Damarla
04avm-res-apimanagement-serviceπŸ“„API Management Serviceswatilekhapaul
Swatilekha Paul
05avm-res-app-containerappπŸ“„Container Applonegunmanb
Zijie He
06avm-res-app-jobπŸ“„App Jobsujaypillai
Sujay Pillai
07avm-res-app-managedenvironmentπŸ“„App Managed Environmentsegraef
Sebastian Graef
08avm-res-appconfiguration-configurationstoreπŸ“„App Configuration Storematt-FFFFFF
Matt White
09avm-res-authorization-roleassignmentπŸ“„Role Assignmentjaredfholgate
Jared Holgate
10avm-res-automation-automationaccountπŸ“„Automation Accountdidayal-msft
Divyadeep Dayal
Poven795909
Poornima Venkataramanan
11avm-res-avs-privatecloudπŸ“„AVS Private Cloudjchancellor-ms
Jon Chancellor
12avm-res-azurestackhci-clusterπŸ“„Azure Stack HCI Clusterchirag1603
Chirag Choudha
13avm-res-azurestackhci-logicalnetworkπŸ“„AzureStackHCI logical networkchirag1603
Chirag Choudha
14avm-res-azurestackhci-virtualmachineinstanceπŸ“„Stack HCI Virtual Machine Instancechirag1603
Chirag Choudha
15avm-res-batch-batchaccountπŸ“„Batch Accountethanjenkins1
Ethan Jenkins
16avm-res-botservice-botserviceπŸ“„Bot Serviceethanjenkins1
Ethan Jenkins
17avm-res-cache-redisπŸ“„Redis Cachejchancellor-ms
Jon Chancellor
18avm-res-cache-redisenterpriseπŸ“„Azure Managed RedisAnkur1106
Ankur Sharma
19avm-res-cdn-profileπŸ“„CDN ProfilePoven795909
Poornima Venkataramanan
didayal-msft
Divyadeep Dayal
20avm-res-certificateregistration-certificateorderπŸ“„Certificate Orderslonegunmanb
Zijie He
21avm-res-cognitiveservices-accountπŸ“„Cognitive Servicelonegunmanb
Zijie He
22avm-res-communication-emailserviceπŸ“„Email Communication Servicelonegunmanb
Zijie He
23avm-res-compute-capacityreservationgroupπŸ“„Capacity Reservation GroupWenAI2020
Wen Tian
24avm-res-compute-diskπŸ“„Compute Diskterrymandin
Terry Mandin
25avm-res-compute-diskencryptionsetπŸ“„Disk Encryption SetAkashc0807
Akash Choudhary
26avm-res-compute-galleryπŸ“„Azure Compute GalleryAkashc0807
Akash Choudhary
27avm-res-compute-hostgroupπŸ“„Host GroupsWenAI2020
Wen Tian
28avm-res-compute-imagen/aImage
29avm-res-compute-proximityplacementgroupπŸ“„Proximity Placement Groupfafriha
Farouk Friha
30avm-res-compute-sshpublickeyπŸ“„Public SSH KeyChrisSidebotham
Chris Sidebotham
31avm-res-compute-virtualmachineπŸ“„Virtual Machine
VM
jchancellor-ms
Jon Chancellor
32avm-res-compute-virtualmachinescalesetπŸ“„Virtual Machine Scale Set
VMSS
terrymandin
Terry Mandin
marcelkmfst
Marcel Keller
33avm-res-consumption-budgetn/aConsumption BudgetAkashc0807
Akash Choudhary
34avm-res-containerinstance-containergroupπŸ“„Container Instancesharmilamusunuru
Sharmila Musunuru
35avm-res-containerregistry-registryπŸ“„Azure Container Registry (ACR)Akashc0807
Akash Choudhary
36avm-res-containerservice-fleetn/aAKS Fleetdidayal-msft
Divyadeep Dayal
amruta53
Amruta Kulkarni
37avm-res-containerservice-managedclusterπŸ“„AKS Managed ClusteribersanoMS
Isabelle Bersano
38avm-res-dashboard-grafanan/aAzure Managed Grafanakhajour
Abdelaziz Khajour
39avm-res-databricks-accessconnectorn/aAzure Databricks Access Connector
40avm-res-databricks-workspaceπŸ“„Azure Databricks Workspacesegraef
Sebastian Graef
41avm-res-datafactory-factoryπŸ“„Data Factoryasishr
Asish R
42avm-res-dataprotection-backupvaultπŸ“„Data Protection Backup Vaultethanjenkins1
Ethan Jenkins
43avm-res-dataprotection-resourceguardπŸ“„Data Protection Resource GuardWenAI2020
Wen Tian
44avm-res-dbformysql-flexibleserverπŸ“„DB for MySQL Flexible Serverelsalvos
Cesar Abrego
45avm-res-dbforpostgresql-flexibleserverπŸ“„DB for Postgre SQL Flexible Serverzaidmohd
Zaid Mohammad
46avm-res-desktopvirtualization-applicationgroupπŸ“„Azure Virtual Desktop (AVD) Application Groupjensheerin
Jen Sheerin
sihbher
Gerardo Reyes
47avm-res-desktopvirtualization-hostpoolπŸ“„Azure Virtual Desktop (AVD) Host Pooljensheerin
Jen Sheerin
sihbher
Gerardo Reyes
48avm-res-desktopvirtualization-scalingplanπŸ“„Azure Virtual Desktop (AVD) Scaling Planjensheerin
Jen Sheerin
sihbher
Gerardo Reyes
49avm-res-desktopvirtualization-workspaceπŸ“„Azure Virtual Desktop (AVD) Workspacejensheerin
Jen Sheerin
sihbher
Gerardo Reyes
50avm-res-devcenter-devcenterπŸ“„Dev Centerabhishekaryams
Abhishek Arya
51avm-res-deviceregistry-assetendpointprofilen/aDevice Registry Asset Endpoint Profilelonegunmanb
Zijie He
52avm-res-devopsinfrastructure-poolπŸ“„DevOps Poolsjaredfholgate
Jared Holgate
53avm-res-devtestlab-labn/aDevTest Labsharmilamusunuru
Sharmila Musunuru
54avm-res-digitaltwins-digitaltwinsinstancen/aDigital Twins Instance
55avm-res-documentdb-databaseaccountπŸ“„CosmosDB Database Accountcmaneu
Christopher Maneu
56avm-res-documentdb-mongoclusterπŸ“„Cosmos DB for MongoDB (vCore)sujaypillai
Sujay Pillai
57avm-res-edge-siteπŸ“„Azure Arc Site managerchirag1603
Chirag Choudha
58avm-res-eventgrid-domainπŸ“„Event Grid Domainsujaypillai
Sujay Pillai
59avm-res-eventgrid-namespaceπŸ“„Event Grid Namespacedassbernd
Berna Sandalli
60avm-res-eventgrid-systemtopicn/aEvent Grid System Topicsujaypillai
Sujay Pillai
61avm-res-eventgrid-topicπŸ“„Event Grid Topicsujaypillai
Sujay Pillai
62avm-res-eventhub-namespaceπŸ“„Event Hub Namespacercavaturu
Raja Kalyan Ram Cavaturu
63avm-res-features-featureπŸ“„Azure Feature Exposure Control (AFEC)lonegunmanb
Zijie He
64avm-res-healthbot-healthbotn/aAzure Health Bot
65avm-res-hybridcompute-machinen/aHybrid Compute Machine
66avm-res-hybridcontainerservice-provisionedclusterinstanceπŸ“„AKS Arcchirag1603
Chirag Choudha
67avm-res-insights-actiongroupn/aAction Grouparjenhuitema
Arjen Huitema
Brunoga-MS
Bruno Gabrielli
68avm-res-insights-activitylogalertn/aActivity log alertstagolovina
Tanya Golovina
69avm-res-insights-autoscalesettingπŸ“„Auto scale settingsMinHeinA
Min Hein Aung
70avm-res-insights-componentπŸ“„Application InsightJfolberth
John Folberth
71avm-res-insights-datacollectionendpointπŸ“„Data Collection Endpointsharmilamusunuru
Sharmila Musunuru
72avm-res-insights-datacollectionrulen/aData Collection Rulesharmilamusunuru
Sharmila Musunuru
73avm-res-insights-logprofilen/aLog profilesharmilamusunuru
Sharmila Musunuru
74avm-res-insights-metricalertn/aMetric Alertarjenhuitema
Arjen Huitema
75avm-res-insights-privatelinkscopen/aAzure Monitor Private Link Scopesharmilamusunuru
Sharmila Musunuru
76avm-res-insights-scheduledqueryrulen/aScheduled Query Rulexsatishx
Satish Balakrishnan
77avm-res-iotoperations-instancen/aAzure IoT Operationsagreaves-ms
Allen Greaves
WilliamBerryiii
Bill Berry
78avm-res-keyvault-vaultπŸ“„Key Vault
KV
matt-FFFFFF
Matt White
79avm-res-kusto-clusterπŸ“„Kusto ClustersLaurentLesle
Laurent Lesle
80avm-res-loadtestservice-loadtestn/aLoad Testing Servicepfayika1
Philippe Fayika
81avm-res-logic-workflowπŸ“„Logic Apps (Workflow)bakrish
Bala Krishnamoorthy
82avm-res-machinelearningservices-workspaceπŸ“„Machine Learning Services Workspace
ML Workspace
Nepomuceno
Gabriel Monteiro Nepomuceno
83avm-res-maintenance-maintenanceconfigurationπŸ“„Maintenance ConfigurationASHR4
Rhys Ash
84avm-res-managedidentity-userassignedidentityπŸ“„User Assigned Identity
MSI
Jfolberth
John Folberth
85avm-res-managedservices-registrationdefinitionn/aRegistration Definition (Lighthouse)
86avm-res-management-managementgroupn/aManagement Group
MG
herms14
Hermes Miraflor II
87avm-res-management-servicegroupπŸ“„Management Service Groupshaflidif
Haflidi Fridthjofsson
88avm-res-netapp-netappaccountπŸ“„Azure NetApp Filejtracey93
Jack Tracey
89avm-res-network-applicationgatewayπŸ“„Application Gateway
App GW
mofaizal
Mohamed Faizal
90avm-res-network-applicationgatewaywebapplicationfirewallpolicyπŸ“„Application Gateway Web Application Firewall (WAF) Policymofaizal
Mohamed Faizal
91avm-res-network-applicationsecuritygroupπŸ“„Application Security Group (ASG)
ASG
MinHeinA
Min Hein Aung
92avm-res-network-azurefirewallπŸ“„Azure Firewall
Azure FW
vmisson
Vincent Misson
93avm-res-network-bastionhostπŸ“„Bastion Hosthumanascode
Itamar Hirosh
94avm-res-network-connectionπŸ“„Virtual Network Gateway Connectionjchancellor-ms
Jon Chancellor
95avm-res-network-ddosprotectionplanπŸ“„DDoS Protectionsitarant
Simona Tarantola
jtracey93
Jack Tracey
96avm-res-network-dnsforwardingrulesetn/aDNS Forwarding Rulesetsharmilamusunuru
Sharmila Musunuru
97avm-res-network-dnsresolverπŸ“„DNS Resolverhumanascode
Itamar Hirosh
98avm-res-network-dnszoneπŸ“„Public DNS Zonesharmilamusunuru
Sharmila Musunuru
99avm-res-network-expressroutecircuitπŸ“„ExpressRoute Circuit
ER
khushal08
Khush Kaviraj
adammontlake
Adam Montlake
100avm-res-network-firewallpolicyπŸ“„Azure Firewall PolicyMinHeinA
Min Hein Aung
101avm-res-network-frontdoorn/aAzure Front Door
102avm-res-network-frontdoorwebapplicationfirewallpolicyπŸ“„Front Door Web Application Firewall (WAF) Policysihbher
Gerardo Reyes
103avm-res-network-ipgroupπŸ“„IP Groupmathewsg
Mathews George
104avm-res-network-loadbalancerπŸ“„Loadbalancerdonovm4
Donovan McCoy
105avm-res-network-localnetworkgatewayπŸ“„Local Network GatewayBhavyasree08
Bhavyasree Damarla
106avm-res-network-natgatewayπŸ“„NAT Gatewayiarik0440
Yarik Simineans
107avm-res-network-networkinterfaceπŸ“„Network Interface
NIC
fafriha
Farouk Friha
108avm-res-network-networkmanagerπŸ“„Azure Virtual Network ManagerMrRoundRobin
Robin Muller
109avm-res-network-networksecuritygroupπŸ“„Network Security Groupmaheshbenke
Mahesh Benke
110avm-res-network-networksecurityperimetern/aNetwork Security Perimetersujaypillai
Sujay Pillai
111avm-res-network-networkwatcherπŸ“„Azure Network Watcherterrymandin
Terry Mandin
112avm-res-network-privatednszoneπŸ“„Private DNS ZoneMinHeinA
Min Hein Aung
113avm-res-network-privateendpointπŸ“„Private Endpointjaredfholgate
Jared Holgate
114avm-res-network-privatelinkservicen/aPrivate Link Serviceavivshrem
Aviv Shrem
115avm-res-network-publicipaddressπŸ“„Public IP Address
PIP
vmisson
Vincent Misson
116avm-res-network-publicipprefixπŸ“„Public IP PrefixPmeshramPM
Pankaj Meshram
117avm-res-network-routetableπŸ“„Route Table
UDR
adammontlake
Adam Montlake
118avm-res-network-serviceendpointpolicyn/aService Endpoint PolicyASHR4
Rhys Ash
119avm-res-network-trafficmanagerprofileπŸ“„Traffic Manager ProfileAnubhaR94
Anubha Rana
120avm-res-network-virtualnetworkπŸ“„Virtual Network
VNET
jaredfholgate
Jared Holgate
121avm-res-network-virtualnetworkgatewayn/aVirtual Network Gateway
VNET GW

122avm-res-network-virtualroutern/aRoute Server
123avm-res-operationalinsights-workspaceπŸ“„Log Analytics workspaceiarik0440
Yarik Simineans
124avm-res-operationsmanagement-solutionn/aOperations Management Solution
125avm-res-oracledatabase-cloudexadatainfrastructureπŸ“„Oracle Exadata Infrastructuresihbher
Gerardo Reyes
terrymandin
Terry Mandin
126avm-res-oracledatabase-cloudvmclusterπŸ“„Oracle VM clustersihbher
Gerardo Reyes
terrymandin
Terry Mandin
127avm-res-portal-dashboardπŸ“„Azure Portal DashboardVeronicaSea
Veronica Xu
128avm-res-powerbidedicated-capacityn/aPower BI Dedicated Capacity
129avm-res-purview-accountn/aPurview Account
130avm-res-recoveryservices-vaultπŸ“„Recovery Services Vaultelsalvos
Cesar Abrego
131avm-res-redhatopenshift-openshiftclusterπŸ“„OpenShift Clusterethanjenkins1
Ethan Jenkins
puneetdevadiga
Puneet Devadiga
132avm-res-relay-namespaceπŸ“„Relay Namespacesujaypillai
Sujay Pillai
133avm-res-resourcegraph-queryπŸ“„Resource Graph QuerySJAYAP
S Jayaprakash
134avm-res-resources-featuren/aResource Featureslonegunmanb
Zijie He
135avm-res-resources-resourcegroupπŸ“„Resource Group
RG
Jfolberth
John Folberth
136avm-res-search-searchserviceπŸ“„Search Servicelestermarch
Lester March
137avm-res-servicebus-namespaceπŸ“„Service Bus Namespaceiarik0440
Yarik Simineans
138avm-res-servicefabric-clustern/aService Fabric Cluster
139avm-res-servicenetworking-trafficcontrollern/aApplication Gateway for Containers (Traffic Controller)mofaizal
Mohamed Faizal
140avm-res-signalrservice-signalrn/aSignalR Service SignalR
141avm-res-sql-instancepooln/aInstance Poolssujaypillai
Sujay Pillai
142avm-res-sql-managedinstanceπŸ“„SQL Managed Instance
SQL MI
chanakanissanka
Chanaka Nissanka
abhishekaryams
Abhishek Arya
143avm-res-sql-serverπŸ“„Azure SQL Serverabhishekaryams
Abhishek Arya
144avm-res-sqlvirtualmachine-sqlvirtualmachineπŸ“„Sql Virtual Machine
SQL VM
sujaypillai
Sujay Pillai
145avm-res-storage-storageaccountπŸ“„Storage Accountchinthakaru
Chinthaka Rupasinghe
146avm-res-synapse-workspacen/aSynapse WorkspaceKarni-G
Karni Gupta
147avm-res-virtualmachineimages-imagetemplaten/aVirtual Machine Image Templatetravishankins
Travis Hankins
148avm-res-web-connectionπŸ“„API Connectiondonovm4
Donovan McCoy
149avm-res-web-hostingenvironmentπŸ“„App Service Environment
ASE
ibersanoMS
Isabelle Bersano
150avm-res-web-serverfarmπŸ“„App Service PlanibersanoMS
Isabelle Bersano
151avm-res-web-siteπŸ“„Web/Function App
App Service, Web Site, Logic App, Function App
donovm4
Donovan McCoy
152avm-res-web-staticsiteπŸ“„Static Web Appdonovm4
Donovan McCoy

Module Publication History - πŸ“…

βž• Module Publication History - Module names, status and owners

Modules published in March 2026

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-botservice-botserviceπŸ“„Bot Serviceethanjenkins1
Ethan Jenkins

Modules published in February 2026

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-cache-redisenterpriseπŸ“„Azure Managed RedisAnkur1106
Ankur Sharma
02avm-res-eventgrid-namespaceπŸ“„Event Grid Namespacedassbernd
Berna Sandalli
03avm-res-network-trafficmanagerprofileπŸ“„Traffic Manager ProfileAnubhaR94
Anubha Rana
04avm-res-sqlvirtualmachine-sqlvirtualmachineπŸ“„Sql Virtual Machine
SQL VM
sujaypillai
Sujay Pillai

Modules published in January 2026

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-eventgrid-domainπŸ“„Event Grid Domainsujaypillai
Sujay Pillai
02avm-res-eventgrid-topicπŸ“„Event Grid Topicsujaypillai
Sujay Pillai
03avm-res-relay-namespaceπŸ“„Relay Namespacesujaypillai
Sujay Pillai

Modules published in November 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-features-featureπŸ“„Azure Feature Exposure Control (AFEC)lonegunmanb
Zijie He
02avm-res-web-connectionπŸ“„API Connectiondonovm4
Donovan McCoy

Modules published in October 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-management-servicegroupπŸ“„Management Service Groupshaflidif
Haflidi Fridthjofsson
02avm-res-redhatopenshift-openshiftclusterπŸ“„OpenShift Clusterethanjenkins1
Ethan Jenkins
puneetdevadiga
Puneet Devadiga

Modules published in August 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-compute-capacityreservationgroupπŸ“„Capacity Reservation GroupWenAI2020
Wen Tian
02avm-res-documentdb-mongoclusterπŸ“„Cosmos DB for MongoDB (vCore)sujaypillai
Sujay Pillai

Modules published in July 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-certificateregistration-certificateorderπŸ“„Certificate Orderslonegunmanb
Zijie He
02avm-res-dataprotection-resourceguardπŸ“„Data Protection Resource GuardWenAI2020
Wen Tian

Modules published in June 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-communication-emailserviceπŸ“„Email Communication Servicelonegunmanb
Zijie He

Modules published in May 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-app-jobπŸ“„App Jobsujaypillai
Sujay Pillai
02avm-res-batch-batchaccountπŸ“„Batch Accountethanjenkins1
Ethan Jenkins
03avm-res-dataprotection-backupvaultπŸ“„Data Protection Backup Vaultethanjenkins1
Ethan Jenkins

Modules published in April 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-apimanagement-serviceπŸ“„API Management Serviceswatilekhapaul
Swatilekha Paul
02avm-res-automation-automationaccountπŸ“„Automation Accountdidayal-msft
Divyadeep Dayal
Poven795909
Poornima Venkataramanan
03avm-res-datafactory-factoryπŸ“„Data Factoryasishr
Asish R
04avm-res-eventhub-namespaceπŸ“„Event Hub Namespacercavaturu
Raja Kalyan Ram Cavaturu
05avm-res-maintenance-maintenanceconfigurationπŸ“„Maintenance ConfigurationASHR4
Rhys Ash
06avm-res-network-ipgroupπŸ“„IP Groupmathewsg
Mathews George
07avm-res-network-publicipprefixπŸ“„Public IP PrefixPmeshramPM
Pankaj Meshram
08avm-res-resourcegraph-queryπŸ“„Resource Graph QuerySJAYAP
S Jayaprakash

Modules published in March 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-appconfiguration-configurationstoreπŸ“„App Configuration Storematt-FFFFFF
Matt White

Modules published in February 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-network-applicationgatewaywebapplicationfirewallpolicyπŸ“„Application Gateway Web Application Firewall (WAF) Policymofaizal
Mohamed Faizal

Modules published in January 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-netapp-netappaccountπŸ“„Azure NetApp Filejtracey93
Jack Tracey
02avm-res-network-applicationsecuritygroupπŸ“„Application Security Group (ASG)
ASG
MinHeinA
Min Hein Aung
03avm-res-network-connectionπŸ“„Virtual Network Gateway Connectionjchancellor-ms
Jon Chancellor
04avm-res-recoveryservices-vaultπŸ“„Recovery Services Vaultelsalvos
Cesar Abrego

Modules published in December 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-compute-galleryπŸ“„Azure Compute GalleryAkashc0807
Akash Choudhary

Modules published in November 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-compute-proximityplacementgroupπŸ“„Proximity Placement Groupfafriha
Farouk Friha
02avm-res-containerservice-managedclusterπŸ“„AKS Managed ClusteribersanoMS
Isabelle Bersano
03avm-res-insights-autoscalesettingπŸ“„Auto scale settingsMinHeinA
Min Hein Aung
04avm-res-network-localnetworkgatewayπŸ“„Local Network GatewayBhavyasree08
Bhavyasree Damarla
05avm-res-network-networkinterfaceπŸ“„Network Interface
NIC
fafriha
Farouk Friha

Modules published in October 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-compute-diskencryptionsetπŸ“„Disk Encryption SetAkashc0807
Akash Choudhary
02avm-res-devcenter-devcenterπŸ“„Dev Centerabhishekaryams
Abhishek Arya
03avm-res-network-expressroutecircuitπŸ“„ExpressRoute Circuit
ER
khushal08
Khush Kaviraj
adammontlake
Adam Montlake
04avm-res-network-frontdoorwebapplicationfirewallpolicyπŸ“„Front Door Web Application Firewall (WAF) Policysihbher
Gerardo Reyes

Modules published in September 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-dbformysql-flexibleserverπŸ“„DB for MySQL Flexible Serverelsalvos
Cesar Abrego
02avm-res-dbforpostgresql-flexibleserverπŸ“„DB for Postgre SQL Flexible Serverzaidmohd
Zaid Mohammad
03avm-res-network-privateendpointπŸ“„Private Endpointjaredfholgate
Jared Holgate
04avm-res-oracledatabase-cloudexadatainfrastructureπŸ“„Oracle Exadata Infrastructuresihbher
Gerardo Reyes
terrymandin
Terry Mandin
05avm-res-oracledatabase-cloudvmclusterπŸ“„Oracle VM clustersihbher
Gerardo Reyes
terrymandin
Terry Mandin
06avm-res-portal-dashboardπŸ“„Azure Portal DashboardVeronicaSea
Veronica Xu
07avm-res-sql-managedinstanceπŸ“„SQL Managed Instance
SQL MI
chanakanissanka
Chanaka Nissanka
abhishekaryams
Abhishek Arya
08avm-res-sql-serverπŸ“„Azure SQL Serverabhishekaryams
Abhishek Arya

Modules published in August 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-azurestackhci-clusterπŸ“„Azure Stack HCI Clusterchirag1603
Chirag Choudha
02avm-res-azurestackhci-logicalnetworkπŸ“„AzureStackHCI logical networkchirag1603
Chirag Choudha
03avm-res-azurestackhci-virtualmachineinstanceπŸ“„Stack HCI Virtual Machine Instancechirag1603
Chirag Choudha
04avm-res-compute-hostgroupπŸ“„Host GroupsWenAI2020
Wen Tian
05avm-res-devopsinfrastructure-poolπŸ“„DevOps Poolsjaredfholgate
Jared Holgate
06avm-res-documentdb-databaseaccountπŸ“„CosmosDB Database Accountcmaneu
Christopher Maneu
07avm-res-edge-siteπŸ“„Azure Arc Site managerchirag1603
Chirag Choudha
08avm-res-hybridcontainerservice-provisionedclusterinstanceπŸ“„AKS Arcchirag1603
Chirag Choudha
09avm-res-insights-componentπŸ“„Application InsightJfolberth
John Folberth
10avm-res-insights-datacollectionendpointπŸ“„Data Collection Endpointsharmilamusunuru
Sharmila Musunuru
11avm-res-network-applicationgatewayπŸ“„Application Gateway
App GW
mofaizal
Mohamed Faizal
12avm-res-network-dnszoneπŸ“„Public DNS Zonesharmilamusunuru
Sharmila Musunuru
13avm-res-resources-resourcegroupπŸ“„Resource Group
RG
Jfolberth
John Folberth
14avm-res-search-searchserviceπŸ“„Search Servicelestermarch
Lester March
15avm-res-web-serverfarmπŸ“„App Service PlanibersanoMS
Isabelle Bersano

Modules published in July 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-app-containerappπŸ“„Container Applonegunmanb
Zijie He
02avm-res-containerinstance-containergroupπŸ“„Container Instancesharmilamusunuru
Sharmila Musunuru
03avm-res-machinelearningservices-workspaceπŸ“„Machine Learning Services Workspace
ML Workspace
Nepomuceno
Gabriel Monteiro Nepomuceno
04avm-res-network-networkwatcherπŸ“„Azure Network Watcherterrymandin
Terry Mandin

Modules published in June 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-cache-redisπŸ“„Redis Cachejchancellor-ms
Jon Chancellor
02avm-res-logic-workflowπŸ“„Logic Apps (Workflow)bakrish
Bala Krishnamoorthy
03avm-res-web-hostingenvironmentπŸ“„App Service Environment
ASE
ibersanoMS
Isabelle Bersano

Modules published in May 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-cdn-profileπŸ“„CDN ProfilePoven795909
Poornima Venkataramanan
didayal-msft
Divyadeep Dayal
02avm-res-compute-diskπŸ“„Compute Diskterrymandin
Terry Mandin
03avm-res-compute-sshpublickeyπŸ“„Public SSH KeyChrisSidebotham
Chris Sidebotham
04avm-res-network-dnsresolverπŸ“„DNS Resolverhumanascode
Itamar Hirosh
05avm-res-network-routetableπŸ“„Route Table
UDR
adammontlake
Adam Montlake

Modules published in April 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-kusto-clusterπŸ“„Kusto ClustersLaurentLesle
Laurent Lesle
02avm-res-servicebus-namespaceπŸ“„Service Bus Namespaceiarik0440
Yarik Simineans

Modules published in March 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-databricks-workspaceπŸ“„Azure Databricks Workspacesegraef
Sebastian Graef
02avm-res-managedidentity-userassignedidentityπŸ“„User Assigned Identity
MSI
Jfolberth
John Folberth
03avm-res-network-privatednszoneπŸ“„Private DNS ZoneMinHeinA
Min Hein Aung

Modules published in February 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-app-managedenvironmentπŸ“„App Managed Environmentsegraef
Sebastian Graef
02avm-res-avs-privatecloudπŸ“„AVS Private Cloudjchancellor-ms
Jon Chancellor
03avm-res-cognitiveservices-accountπŸ“„Cognitive Servicelonegunmanb
Zijie He
04avm-res-compute-virtualmachinescalesetπŸ“„Virtual Machine Scale Set
VMSS
terrymandin
Terry Mandin
marcelkmfst
Marcel Keller
05avm-res-containerregistry-registryπŸ“„Azure Container Registry (ACR)Akashc0807
Akash Choudhary
06avm-res-network-bastionhostπŸ“„Bastion Hosthumanascode
Itamar Hirosh
07avm-res-network-networksecuritygroupπŸ“„Network Security Groupmaheshbenke
Mahesh Benke
08avm-res-network-publicipaddressπŸ“„Public IP Address
PIP
vmisson
Vincent Misson
09avm-res-storage-storageaccountπŸ“„Storage Accountchinthakaru
Chinthaka Rupasinghe
10avm-res-web-siteπŸ“„Web/Function App
App Service, Web Site, Logic App, Function App
donovm4
Donovan McCoy
11avm-res-web-staticsiteπŸ“„Static Web Appdonovm4
Donovan McCoy

Modules published in January 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-authorization-roleassignmentπŸ“„Role Assignmentjaredfholgate
Jared Holgate
02avm-res-network-azurefirewallπŸ“„Azure Firewall
Azure FW
vmisson
Vincent Misson
03avm-res-network-firewallpolicyπŸ“„Azure Firewall PolicyMinHeinA
Min Hein Aung
04avm-res-network-networkmanagerπŸ“„Azure Virtual Network ManagerMrRoundRobin
Robin Muller
05avm-res-operationalinsights-workspaceπŸ“„Log Analytics workspaceiarik0440
Yarik Simineans

Modules published in December 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-desktopvirtualization-applicationgroupπŸ“„Azure Virtual Desktop (AVD) Application Groupjensheerin
Jen Sheerin
sihbher
Gerardo Reyes
02avm-res-desktopvirtualization-scalingplanπŸ“„Azure Virtual Desktop (AVD) Scaling Planjensheerin
Jen Sheerin
sihbher
Gerardo Reyes
03avm-res-desktopvirtualization-workspaceπŸ“„Azure Virtual Desktop (AVD) Workspacejensheerin
Jen Sheerin
sihbher
Gerardo Reyes
04avm-res-network-natgatewayπŸ“„NAT Gatewayiarik0440
Yarik Simineans

Modules published in November 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-compute-virtualmachineπŸ“„Virtual Machine
VM
jchancellor-ms
Jon Chancellor
02avm-res-network-ddosprotectionplanπŸ“„DDoS Protectionsitarant
Simona Tarantola
jtracey93
Jack Tracey
03avm-res-network-loadbalancerπŸ“„Loadbalancerdonovm4
Donovan McCoy

Modules published in October 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-desktopvirtualization-hostpoolπŸ“„Azure Virtual Desktop (AVD) Host Pooljensheerin
Jen Sheerin
sihbher
Gerardo Reyes
02avm-res-network-virtualnetworkπŸ“„Virtual Network
VNET
jaredfholgate
Jared Holgate

Modules published in September 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-res-keyvault-vaultπŸ“„Key Vault
KV
matt-FFFFFF
Matt White

Terraform Pattern Modules

Module catalog

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
TerraformPattern252651
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Info

This page contains various views of the module index (catalog) for Terraform Pattern Modules. To see these views, click on the expandable sections with the “βž•” sign below.

  • To see the full, unfiltered, unformatted module index on GitHub, click here.

  • To download the source CSV file, click here.

Note

Modules listed below that aren’t shown with the status of Module Available 🟒, are currently in development and are not yet available for use. For proposed modules, see the Proposed modules section below.

Published modules - 🟒 & 🟑

βž• Published Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-ptn-aca-lza-hosting-environmentπŸ“„Azure Container Apps Landing Zone Acceleratorsam-cogan
Sam Cogan
02avm-ptn-aiml-ai-foundryπŸ“„AI-ML - AI Foundrysegraef
Sebastian Graef
mbilalamjad
Bilal Amjad
03avm-ptn-aiml-landing-zoneπŸ“„AI-ML - Landing Zone (LZ)jchancellor-ms
Jon Chancellor
mbilalamjad
Bilal Amjad
04avm-ptn-aks-devπŸ“„AKS devms-henglu
Heng Lu
05avm-ptn-aks-economyπŸ“„AKS economyms-henglu
Heng Lu
06avm-ptn-aks-enterpriseπŸ“„AKS Enterprisems-henglu
Heng Lu
07avm-ptn-aks-productionπŸ“„Azure Kubernetes Servicechanakanissanka
Chanaka Nissanka
08avm-ptn-alzπŸ“„Azure Landing Zone Management Groups and Policy
ALZ Management Groups and Policy
matt-FFFFFF
Matt White
09avm-ptn-alz-connectivity-hub-and-spoke-vnetπŸ“„ALZ Connectivity Hub and Spoke
Azure Landing Zones - Hub and Spoke
jaredfholgate
Jared Holgate
10avm-ptn-alz-connectivity-virtual-wanπŸ“„ALZ Connectivity vWAN
Azure Landing Zones - vWAN
jaredfholgate
Jared Holgate
11avm-ptn-alz-managementπŸ“„ALZ Management
Azure Landing Zones - Management
matt-FFFFFF
Matt White
12avm-ptn-alz-sub-vendingπŸ“„ALZ Subscription vending
Azure Landing Zones - Sub vending
matt-FFFFFF
Matt White
jaredfholgate
Jared Holgate
13avm-ptn-avd-lza-insightsπŸ“„AVD Insights
Azure Virtual Desktop Insights
jensheerin
Jen Sheerin
sihbher
Gerardo Reyes
14avm-ptn-avd-lza-managementplaneπŸ“„AVD Management Plane
Azure Virtual Desktop Management Plane
jensheerin
Jen Sheerin
sihbher
Gerardo Reyes
15avm-ptn-azuremonitorwindowsagentπŸ“„Azure Monitor Windows Agentchirag1603
Chirag Choudha
16avm-ptn-cicd-agents-and-runnersπŸ“„CI CD Agents and Runnersjaredfholgate
Jared Holgate
17avm-ptn-ephemeral-credentialπŸ“„Ephemeral Credentials Generatorlonegunmanb
Zijie He
18avm-ptn-function-app-storage-private-endpointsπŸ“„Function App and private endpoint-secured Storagedonovm4
Donovan McCoy
19avm-ptn-hci-ad-provisionerπŸ“„Arc for AD registrationchirag1603
Chirag Choudha
20avm-ptn-hci-server-provisionerπŸ“„Arc for Server registrationchirag1603
Chirag Choudha
21avm-ptn-monitoring-amba-alzπŸ“„AMBA ALZ Pattern
Azure Monitor Baseline Alerts - Azure Landing Zones Pattern
arjenhuitema
Arjen Huitema
Brunoga-MS
Bruno Gabrielli
22avm-ptn-network-private-link-private-dns-zonesπŸ“„Private Link Private DNS Zonesjtracey93
Jack Tracey
23avm-ptn-network-routeserverπŸ“„Azure Route Serverjchancellor-ms
Jon Chancellor
24avm-ptn-odaaπŸ“„Oracle Exedata Workloadterrymandin
Terry Mandin
25avm-ptn-policyassignmentπŸ“„Policy assignmentbjornhofer
Bjorn Hofer

Proposed modules - βšͺ

βž• Proposed Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-ptn-ai-platform-baselinen/aAI platform baselineNepomuceno
Gabriel Monteiro Nepomuceno
02avm-ptn-alz-application-landing-zone-cicd-bootstrap-azure-devopsn/aALZ Application Landing Zone - CICD Bootstrap for Azure DevOpsjaredfholgate
Jared Holgate
03avm-ptn-alz-application-landing-zone-cicd-bootstrap-githubn/aALZ Application Landing Zone - CICD Bootstrap for GitHubjaredfholgate
Jared Holgate
04avm-ptn-alz-application-landing-zone-identity-and-accessn/aALZ Identity and Access Managementjaredfholgate
Jared Holgate
matt-FFFFFF
Matt White
05avm-ptn-alz-identityn/aALZ Identity
Azure Landing Zones - Identity

06avm-ptn-alz-policy-exemptionsn/aALZ Policy Exemptionsjaredfholgate
Jared Holgate
matt-FFFFFF
Matt White
07avm-ptn-app-iaas-vm-cosmosdb-tier-fourn/aWorkload - IaaS VM Cosmos DB - Tier 4mikestiers
Mike Stiers
andbron
Andrew Lambert
08avm-ptn-app-service-landing-zonen/aApp Service Landing Zonejaredfholgate
Jared Holgate
09avm-ptn-avd-lza-sessionhostsn/aAVD Session Hosts
Azure Virtual Desktop Session Hosts

10avm-ptn-azure-ipamn/aIPAM
IP Address Management

11avm-ptn-bcdr-vm-replicationn/aAzure Site Recovery VM Replication
12avm-ptn-cicd-bootstrapn/aCI CD bootstrap
13avm-ptn-cloudshell-vnetn/aAzure Cloud Shell in a Virtual Networktravishankins
Travis Hankins
14avm-ptn-commercial-marketplacen/aCommercial Marketplace SaaS Acceleratorethanjenkins1
Ethan Jenkins
15avm-ptn-confidential-computen/aAzure Confidential Compute
16avm-ptn-dev-center-dev-boxn/aMicrosoft Dev Center Dev Boxautocloudarc
Preston Parsard
17avm-ptn-lbvmssn/aVirtual Machine Scale Setterrymandin
Terry Mandin
18avm-ptn-mongodb-atlas-lzan/aMongoDB Atlas on Azure - Landing Zone (LZ)cloud-architect-dev
Deven Wagle
19avm-ptn-odaa-identityn/aOracle Identityterrymandin
Terry Mandin
kohei3110
Kohei Saito
20avm-ptn-openai-cognitivesearchn/aCorporate Line of Business (LoB) ChatBotmikestiers
Mike Stiers
21avm-ptn-openai-e2e-baselinen/aBaseline OpenAI end-to-end chat
22avm-ptn-oracle-iaasn/aOracle Database on Azuresihbher
Gerardo Reyes
23avm-ptn-purestorage-cbs-arrayn/aPure Storage Cloud Block Store on Azuresundarb19
Sundar Balaji Anantharamakrishnan
24avm-ptn-sentinel-solutionsn/aSentinel SolutionsLaurentLesle
Laurent Lesle
25avm-ptn-subnets-nsgs-routesn/aNetwork Security Groups
NSG
swathialuganti
Swathi Aluganti Narasimhulu
26avm-ptn-subscription-service-health-alertsn/aSubscriptions Service Health AlertsASHR4
Rhys Ash

Deprecated modules - πŸ”΄

βž• Deprecated Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-ptn-hubnetworkingπŸ“„Hub Networking
02avm-ptn-virtualwanπŸ“„Virtual WAN
vWAN

03avm-ptn-vnetgatewayπŸ“„Virtual Network Gateway
VNET GW


All modules - πŸ“‡

βž• All Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-ptn-aca-lza-hosting-environmentπŸ“„Azure Container Apps Landing Zone Acceleratorsam-cogan
Sam Cogan
02avm-ptn-ai-platform-baselinen/aAI platform baselineNepomuceno
Gabriel Monteiro Nepomuceno
03avm-ptn-aiml-ai-foundryπŸ“„AI-ML - AI Foundrysegraef
Sebastian Graef
mbilalamjad
Bilal Amjad
04avm-ptn-aiml-landing-zoneπŸ“„AI-ML - Landing Zone (LZ)jchancellor-ms
Jon Chancellor
mbilalamjad
Bilal Amjad
05avm-ptn-aks-devπŸ“„AKS devms-henglu
Heng Lu
06avm-ptn-aks-economyπŸ“„AKS economyms-henglu
Heng Lu
07avm-ptn-aks-enterpriseπŸ“„AKS Enterprisems-henglu
Heng Lu
08avm-ptn-aks-productionπŸ“„Azure Kubernetes Servicechanakanissanka
Chanaka Nissanka
09avm-ptn-alzπŸ“„Azure Landing Zone Management Groups and Policy
ALZ Management Groups and Policy
matt-FFFFFF
Matt White
10avm-ptn-alz-application-landing-zone-cicd-bootstrap-azure-devopsn/aALZ Application Landing Zone - CICD Bootstrap for Azure DevOpsjaredfholgate
Jared Holgate
11avm-ptn-alz-application-landing-zone-cicd-bootstrap-githubn/aALZ Application Landing Zone - CICD Bootstrap for GitHubjaredfholgate
Jared Holgate
12avm-ptn-alz-application-landing-zone-identity-and-accessn/aALZ Identity and Access Managementjaredfholgate
Jared Holgate
matt-FFFFFF
Matt White
13avm-ptn-alz-connectivity-hub-and-spoke-vnetπŸ“„ALZ Connectivity Hub and Spoke
Azure Landing Zones - Hub and Spoke
jaredfholgate
Jared Holgate
14avm-ptn-alz-connectivity-virtual-wanπŸ“„ALZ Connectivity vWAN
Azure Landing Zones - vWAN
jaredfholgate
Jared Holgate
15avm-ptn-alz-identityn/aALZ Identity
Azure Landing Zones - Identity

16avm-ptn-alz-managementπŸ“„ALZ Management
Azure Landing Zones - Management
matt-FFFFFF
Matt White
17avm-ptn-alz-policy-exemptionsn/aALZ Policy Exemptionsjaredfholgate
Jared Holgate
matt-FFFFFF
Matt White
18avm-ptn-alz-sub-vendingπŸ“„ALZ Subscription vending
Azure Landing Zones - Sub vending
matt-FFFFFF
Matt White
jaredfholgate
Jared Holgate
19avm-ptn-app-iaas-vm-cosmosdb-tier-fourn/aWorkload - IaaS VM Cosmos DB - Tier 4mikestiers
Mike Stiers
andbron
Andrew Lambert
20avm-ptn-app-service-landing-zonen/aApp Service Landing Zonejaredfholgate
Jared Holgate
21avm-ptn-avd-lza-insightsπŸ“„AVD Insights
Azure Virtual Desktop Insights
jensheerin
Jen Sheerin
sihbher
Gerardo Reyes
22avm-ptn-avd-lza-managementplaneπŸ“„AVD Management Plane
Azure Virtual Desktop Management Plane
jensheerin
Jen Sheerin
sihbher
Gerardo Reyes
23avm-ptn-avd-lza-sessionhostsn/aAVD Session Hosts
Azure Virtual Desktop Session Hosts

24avm-ptn-azure-ipamn/aIPAM
IP Address Management

25avm-ptn-azuremonitorwindowsagentπŸ“„Azure Monitor Windows Agentchirag1603
Chirag Choudha
26avm-ptn-bcdr-vm-replicationn/aAzure Site Recovery VM Replication
27avm-ptn-cicd-agents-and-runnersπŸ“„CI CD Agents and Runnersjaredfholgate
Jared Holgate
28avm-ptn-cicd-bootstrapn/aCI CD bootstrap
29avm-ptn-cloudshell-vnetn/aAzure Cloud Shell in a Virtual Networktravishankins
Travis Hankins
30avm-ptn-commercial-marketplacen/aCommercial Marketplace SaaS Acceleratorethanjenkins1
Ethan Jenkins
31avm-ptn-confidential-computen/aAzure Confidential Compute
32avm-ptn-dev-center-dev-boxn/aMicrosoft Dev Center Dev Boxautocloudarc
Preston Parsard
33avm-ptn-ephemeral-credentialπŸ“„Ephemeral Credentials Generatorlonegunmanb
Zijie He
34avm-ptn-function-app-storage-private-endpointsπŸ“„Function App and private endpoint-secured Storagedonovm4
Donovan McCoy
35avm-ptn-hci-ad-provisionerπŸ“„Arc for AD registrationchirag1603
Chirag Choudha
36avm-ptn-hci-server-provisionerπŸ“„Arc for Server registrationchirag1603
Chirag Choudha
37avm-ptn-hubnetworkingπŸ“„Hub Networking
38avm-ptn-lbvmssn/aVirtual Machine Scale Setterrymandin
Terry Mandin
39avm-ptn-mongodb-atlas-lzan/aMongoDB Atlas on Azure - Landing Zone (LZ)cloud-architect-dev
Deven Wagle
40avm-ptn-monitoring-amba-alzπŸ“„AMBA ALZ Pattern
Azure Monitor Baseline Alerts - Azure Landing Zones Pattern
arjenhuitema
Arjen Huitema
Brunoga-MS
Bruno Gabrielli
41avm-ptn-network-private-link-private-dns-zonesπŸ“„Private Link Private DNS Zonesjtracey93
Jack Tracey
42avm-ptn-network-routeserverπŸ“„Azure Route Serverjchancellor-ms
Jon Chancellor
43avm-ptn-odaaπŸ“„Oracle Exedata Workloadterrymandin
Terry Mandin
44avm-ptn-odaa-identityn/aOracle Identityterrymandin
Terry Mandin
kohei3110
Kohei Saito
45avm-ptn-openai-cognitivesearchn/aCorporate Line of Business (LoB) ChatBotmikestiers
Mike Stiers
46avm-ptn-openai-e2e-baselinen/aBaseline OpenAI end-to-end chat
47avm-ptn-oracle-iaasn/aOracle Database on Azuresihbher
Gerardo Reyes
48avm-ptn-policyassignmentπŸ“„Policy assignmentbjornhofer
Bjorn Hofer
49avm-ptn-purestorage-cbs-arrayn/aPure Storage Cloud Block Store on Azuresundarb19
Sundar Balaji Anantharamakrishnan
50avm-ptn-sentinel-solutionsn/aSentinel SolutionsLaurentLesle
Laurent Lesle
51avm-ptn-subnets-nsgs-routesn/aNetwork Security Groups
NSG
swathialuganti
Swathi Aluganti Narasimhulu
52avm-ptn-subscription-service-health-alertsn/aSubscriptions Service Health AlertsASHR4
Rhys Ash
53avm-ptn-virtualwanπŸ“„Virtual WAN
vWAN

54avm-ptn-vnetgatewayπŸ“„Virtual Network Gateway
VNET GW


Module Publication History - πŸ“…

βž• Module Publication History - Module names, status and owners

Modules published in February 2026

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-aca-lza-hosting-environmentπŸ“„Azure Container Apps Landing Zone Acceleratorsam-cogan
Sam Cogan

Modules published in January 2026

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-alz-sub-vendingπŸ“„ALZ Subscription vending
Azure Landing Zones - Sub vending
matt-FFFFFF
Matt White
jaredfholgate
Jared Holgate

Modules published in November 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-ephemeral-credentialπŸ“„Ephemeral Credentials Generatorlonegunmanb
Zijie He

Modules published in September 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-aiml-landing-zoneπŸ“„AI-ML - Landing Zone (LZ)jchancellor-ms
Jon Chancellor
mbilalamjad
Bilal Amjad

Modules published in July 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-aiml-ai-foundryπŸ“„AI-ML - AI Foundrysegraef
Sebastian Graef
mbilalamjad
Bilal Amjad

Modules published in April 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-alz-connectivity-virtual-wanπŸ“„ALZ Connectivity vWAN
Azure Landing Zones - vWAN
jaredfholgate
Jared Holgate
02avm-ptn-function-app-storage-private-endpointsπŸ“„Function App and private endpoint-secured Storagedonovm4
Donovan McCoy
03avm-ptn-monitoring-amba-alzπŸ“„AMBA ALZ Pattern
Azure Monitor Baseline Alerts - Azure Landing Zones Pattern
arjenhuitema
Arjen Huitema
Brunoga-MS
Bruno Gabrielli

Modules published in March 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-alz-connectivity-hub-and-spoke-vnetπŸ“„ALZ Connectivity Hub and Spoke
Azure Landing Zones - Hub and Spoke
jaredfholgate
Jared Holgate

Modules published in November 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-aks-economyπŸ“„AKS economyms-henglu
Heng Lu
02avm-ptn-aks-enterpriseπŸ“„AKS Enterprisems-henglu
Heng Lu

Modules published in October 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-aks-devπŸ“„AKS devms-henglu
Heng Lu
02avm-ptn-odaaπŸ“„Oracle Exedata Workloadterrymandin
Terry Mandin
03avm-ptn-policyassignmentπŸ“„Policy assignmentbjornhofer
Bjorn Hofer

Modules published in September 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-hubnetworkingn/aHub Networking

Modules published in August 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-azuremonitorwindowsagentπŸ“„Azure Monitor Windows Agentchirag1603
Chirag Choudha
02avm-ptn-cicd-agents-and-runnersπŸ“„CI CD Agents and Runnersjaredfholgate
Jared Holgate
03avm-ptn-hci-ad-provisionerπŸ“„Arc for AD registrationchirag1603
Chirag Choudha
04avm-ptn-hci-server-provisionerπŸ“„Arc for Server registrationchirag1603
Chirag Choudha

Modules published in June 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-network-private-link-private-dns-zonesπŸ“„Private Link Private DNS Zonesjtracey93
Jack Tracey
02avm-ptn-network-routeserverπŸ“„Azure Route Serverjchancellor-ms
Jon Chancellor

Modules published in May 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-avd-lza-insightsπŸ“„AVD Insights
Azure Virtual Desktop Insights
jensheerin
Jen Sheerin
sihbher
Gerardo Reyes
02avm-ptn-avd-lza-managementplaneπŸ“„AVD Management Plane
Azure Virtual Desktop Management Plane
jensheerin
Jen Sheerin
sihbher
Gerardo Reyes

Modules published in April 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-aks-productionπŸ“„Azure Kubernetes Servicechanakanissanka
Chanaka Nissanka

Modules published in December 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-alz-managementπŸ“„ALZ Management
Azure Landing Zones - Management
matt-FFFFFF
Matt White

Modules published in November 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-alzπŸ“„Azure Landing Zone Management Groups and Policy
ALZ Management Groups and Policy
matt-FFFFFF
Matt White
02avm-ptn-vnetgatewayn/aVirtual Network Gateway
VNET GW


Modules published in October 2023

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-ptn-virtualwann/aVirtual WAN
vWAN


Terraform Utility Modules

Module catalog

LanguageClassificationPublished 🟒 & 🟑Proposed βšͺSUM
TerraformUtility11314
βž• Additional information
Legend

Summary of status icons used on this page

IconStatusDescription
βšͺProposed modulesModules that are proposed and/or being worked on but not published yet.
🟒 & 🟑Published modulesAvailable (🟒) and Orphaned (🟑) modules that are active and usable.
πŸ”΄Deprecated modulesModules that reached the end of their lifecycle.
πŸ“‡All modulesIncluding Published, Proposed and Deprecated ones.

See the Module Lifecycle page for more details.

Info

This page contains various views of the module index (catalog) for Terraform Utility Modules. To see these views, click on the expandable sections with the “βž•” sign below.

  • To see the full, unfiltered, unformatted module index on GitHub, click here.

  • To download the source CSV file, click here.

Note

Modules listed below that aren’t shown with the status of Module Available 🟒, are currently in development and are not yet available for use. For proposed modules, see the Proposed modules section below.

Published modules - 🟒 & 🟑

βž• Published Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-utl-compute-linuxvirtualmachine-azapi-replicatorπŸ“„Linux VM AzRM to AzAPI Replicatorlonegunmanb
Zijie He
02avm-utl-compute-orchestratedvirtualmachinescaleset-azapi-replicatorπŸ“„VMSS AzRM to AzAPI Replicatorlonegunmanb
Zijie He
03avm-utl-compute-windowsvirtualmachine-azapi-replicatorπŸ“„Windows VM AzRM to AzAPI Replicatorlonegunmanb
Zijie He
04avm-utl-interfacesπŸ“„AVM Interfacesmatt-FFFFFF
Matt White
05avm-utl-network-ip-addressesπŸ“„AVM Network IP Addresses
IPv4 CIDR
jaredfholgate
Jared Holgate
06avm-utl-network-virtualnetwork-azapi-replicatorπŸ“„Virtual Network AzRM to AzAPI Replicatorlonegunmanb
Zijie He
07avm-utl-privatedns-privatednszone-azapi-replicatorπŸ“„Private DNS Zone AzRM to AzAPI Replicatorlonegunmanb
Zijie He
08avm-utl-regionsπŸ“„Azure Regions Datamatt-FFFFFF
Matt White
09avm-utl-resources-resourcegroup-azapi-replicatorπŸ“„Resource Group AzRM to AzAPI Replicatorlonegunmanb
Zijie He
10avm-utl-roledefinitionsπŸ“„Azure Role Definitionsmatt-FFFFFF
Matt White
11avm-utl-sku-finderπŸ“„AVM SKU Finder
Sku
jchancellor-ms
Jon Chancellor

Proposed modules - βšͺ

βž• Proposed Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-utl-containerregistry-containerregistry-azapi-replicatorn/aContainer Registry AzRM to AzAPI Replicatorlonegunmanb
Zijie He
02avm-utl-namingn/aModule NamingNepomuceno
Gabriel Monteiro Nepomuceno
matt-FFFFFF
Matt White
03avm-utl-network-subnet-azapi-replicatorn/aSubnet AzRM to AzAPI Replicatorlonegunmanb
Zijie He

Deprecated modules - πŸ”΄

βž• Deprecated Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01❌ None listed❌ None listed❌ None listed❌ None listed❌ None listed

All modules - πŸ“‡

βž• All Modules - Module names, status and owners
No.Module NameSource
Code
Display NameStatus & VersionsPrimary Owner
01avm-utl-compute-linuxvirtualmachine-azapi-replicatorπŸ“„Linux VM AzRM to AzAPI Replicatorlonegunmanb
Zijie He
02avm-utl-compute-orchestratedvirtualmachinescaleset-azapi-replicatorπŸ“„VMSS AzRM to AzAPI Replicatorlonegunmanb
Zijie He
03avm-utl-compute-windowsvirtualmachine-azapi-replicatorπŸ“„Windows VM AzRM to AzAPI Replicatorlonegunmanb
Zijie He
04avm-utl-containerregistry-containerregistry-azapi-replicatorn/aContainer Registry AzRM to AzAPI Replicatorlonegunmanb
Zijie He
05avm-utl-interfacesπŸ“„AVM Interfacesmatt-FFFFFF
Matt White
06avm-utl-namingn/aModule NamingNepomuceno
Gabriel Monteiro Nepomuceno
matt-FFFFFF
Matt White
07avm-utl-network-ip-addressesπŸ“„AVM Network IP Addresses
IPv4 CIDR
jaredfholgate
Jared Holgate
08avm-utl-network-subnet-azapi-replicatorn/aSubnet AzRM to AzAPI Replicatorlonegunmanb
Zijie He
09avm-utl-network-virtualnetwork-azapi-replicatorπŸ“„Virtual Network AzRM to AzAPI Replicatorlonegunmanb
Zijie He
10avm-utl-privatedns-privatednszone-azapi-replicatorπŸ“„Private DNS Zone AzRM to AzAPI Replicatorlonegunmanb
Zijie He
11avm-utl-regionsπŸ“„Azure Regions Datamatt-FFFFFF
Matt White
12avm-utl-resources-resourcegroup-azapi-replicatorπŸ“„Resource Group AzRM to AzAPI Replicatorlonegunmanb
Zijie He
13avm-utl-roledefinitionsπŸ“„Azure Role Definitionsmatt-FFFFFF
Matt White
14avm-utl-sku-finderπŸ“„AVM SKU Finder
Sku
jchancellor-ms
Jon Chancellor

Module Publication History - πŸ“…

βž• Module Publication History - Module names, status and owners

Modules published in January 2026

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-utl-compute-linuxvirtualmachine-azapi-replicatorπŸ“„Linux VM AzRM to AzAPI Replicatorlonegunmanb
Zijie He
02avm-utl-compute-orchestratedvirtualmachinescaleset-azapi-replicatorπŸ“„VMSS AzRM to AzAPI Replicatorlonegunmanb
Zijie He
03avm-utl-compute-windowsvirtualmachine-azapi-replicatorπŸ“„Windows VM AzRM to AzAPI Replicatorlonegunmanb
Zijie He
04avm-utl-network-virtualnetwork-azapi-replicatorπŸ“„Virtual Network AzRM to AzAPI Replicatorlonegunmanb
Zijie He
05avm-utl-privatedns-privatednszone-azapi-replicatorπŸ“„Private DNS Zone AzRM to AzAPI Replicatorlonegunmanb
Zijie He
06avm-utl-resources-resourcegroup-azapi-replicatorπŸ“„Resource Group AzRM to AzAPI Replicatorlonegunmanb
Zijie He

Modules published in September 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-utl-roledefinitionsπŸ“„Azure Role Definitionsmatt-FFFFFF
Matt White

Modules published in March 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-utl-network-ip-addressesπŸ“„AVM Network IP Addresses
IPv4 CIDR
jaredfholgate
Jared Holgate

Modules published in January 2025

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-utl-interfacesπŸ“„AVM Interfacesmatt-FFFFFF
Matt White

Modules published in December 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-utl-sku-finderπŸ“„AVM SKU Finder
Sku
jchancellor-ms
Jon Chancellor

Modules published in August 2024

No.Module NameSource
Code
Display NameStatus & VersionsOwner(s)
01avm-utl-regionsπŸ“„Azure Regions Datamatt-FFFFFF
Matt White

Subsections of Usage Guide

Concepts

Note

This page is a work in progress and will be updated as we improve & finalize the content. Please check back regularly for updates.

When developing an Azure solution using AVM modules, there are several aspects to consider. This page covers important concepts and provides guidance the technical decisions. Each concept/topic referenced here will be further detailed in the corresponding Bicep or Terraform specific guidance.

Language-agnostic concepts

Topics/concepts that are relevant and applicable for both Bicep and Terraform.

Module Sourcing

Public Registry

Leveraging the public registries (i.e., the Bicep Public Registry or the Terraform Public Registry) is the most common and recommended approach.

This allows you to leverage the latest and greatest features of the AVM modules, as well as the latest security updates. While there aren’t any prerequisites for using the public registry - no extra software component or service needs to be installed and no configuration is needed - the client machine the deployment is initiated from will need to have access to the public registry.

Private Registry (synced)

A private registry - that is hosted in your own environment - can store modules originating from the public registry. Using a private registry still grants you the latest version of AVM modules while allowing you to review each version of each module before admitting them to your private registry. You also have control over who can access your own private registry. Note that using a private registry means that you’re still using each module as is, without making any changes.

Inner-sourcing

Inner-sourcing AVM means maintaining your own, synchronized copy of AVM modules in your own internal private registry, repositories or other storage option. Customers normally look to inner-source AVM modules when they have strict security and compliance requirements, or when they want to publish their own lightly wrapped versions of the modules to meet their specific needs; for example changing some allowed or default values for parameter or variable inputs.

This is a more complex approach and requires more effort to maintain, but it can be beneficial in certain scenarios, however, it should not be the default approach as it can lead to a lot of overhead and maintenance and requires significant skills and resources to set up and maintain.

There are many ways to approach inner-sourcing AVM modules for both Bicep and Terraform. The AVM team will be publishing guidance on this topic, based on customer experience and learnings.

Tip

You can see the AVM team talking about inner-sourcing on the AVM February 2025 community call on YouTube.

Solution Development

This section provides advanced guidance for developing solutions using Azure Verified Modules (AVM). It covers technical decisions and concepts that are important for building and deploying Azure solutions using AVM modules.

Planning your solution

When implementing infrastructure in Azure leveraging IaaS and PaaS services, there are multiple options for Azure deployments. In this article we assume that a decision has been made to implement your solution, using Infrastructure-as-Code (IaC). This is best suited to allow programmatic declarative control of the target infrastructure and is ideal for projects that require repeatability and idempotency.

Choosing an Infrastructure-as-Code language

There are multiple language choices when implementing your solution using IaC in Azure. The Azure Verified Modules project currently supports Bicep and Terraform. The following guidance summarizes considerations that can help choose the option that best suits your requirements.

Reasons to choose Bicep

Bicep is the Microsoft 1st party offering for IaC deployments. It supports Generally Available (GA) and preview features for all Azure resources and allows for modular composition of resources and solution templates. The use of simplified syntax makes IaC development intuitive and the use of the Bicep extension for VSCode provides IntelliSense and syntax validation to assist with coding. Finally, Bicep is well suited for infrastructure projects and teams that don’t require management of other cloud platforms or services outside of Azure. For a more detailed read on reasons to choose Bicep, read this article from the Bicep documentation.

Reasons to choose Terraform

HashiCorp’s Terraform is an extensible 3rd party platform that can be used across multiple cloud and on-premises platforms using multiple provider plugins. It has widespread adoption due to its simplified human-readable configuration files, common functionality, and the ability to allow a project to span multiple provider spaces.

In Azure, support is provided through two primary providers called AzureRM and AzAPI respectively. The default provider for many Azure use cases is AzureRM which is co-developed between Microsoft and HashiCorp. It includes support for generally available (GA) features, while support for new and preview features might be slightly delayed following their initial release. AzAPI is developed exclusively by Microsoft and supports all preview and GA features while being more complex to use due to the more direct interaction with Azure’s APIs. While it is possible to use both providers in a single project as needed, the best practice is to standardize on a single provider as much as is reasonable.

Projects typically choose Terraform when they bridge multiple cloud infrastructure platforms or when the development team has previous experience coding in Terraform. Modern Integrated Development Environments (IDE) - such as Visual Studio Code - include extension support for Terraform features as well as additional Azure specific extensions. These extensions enable syntax validation and highlighting as well as code formatting and HashiCorp Cloud Platform (HCP) integration for HashiCorp Cloud customers. For a more detailed read on reasons to choose Terraform, read this article from the Terraform on Azure documentation.

Architecture design

Before starting the process of codifying infrastructure, it is important to develop a detailed architecture of what will be created. This should include details for:

  1. Organizational elements such as management groups, subscriptions, and resource groups as well as any tagging and Role Based Access (RBAC) configurations for each.
  2. Infrastructure services that will be created along with key configuration details like sku values, network CIDR range sizes, or other solution specific configuration.
  3. Any relationship between services that will be codified as part of the deployment.
  4. Identify inputs to your solution for designs that are intended to be used as templates.
Note

For a production grade solution, you need to

  • follow the recommendations of the Cloud Adoption Framework (CAF) and have your platform and application landing zones defined, as per Azure Landing Zones (ALZ);
  • follow the recommendations of the Azure Well-Architected Framework (WAF) to ensure that your solution is compliant with and integrates into your organization’s policies and standards. This includes considerations for security, identity, networking, monitoring, cost management, and governance.

Sourcing content for deployment

Once the architecture is agreed upon, it is time to plan the development of your IaC code. There are several key decision points that should be considered during this phase.

Content creation methods

The two primary methods used to create your solutions module are:

  1. Using base resources (“vanilla resources”) from scratch or
  2. Leveraging pre-created modules from the AVM library to minimize the time to value during development.

The trade-off between the two options is primarily around control vs. speed. AVM works to provide the best of both options by providing modules with opinionated and recommended practice defaults while allowing for more detailed configuration as needed. In our sample exercise we’ll be using AVM modules to demonstrate building the example solution.

AVM module type considerations

When using AVM modules for your solution, there is an additional choice that should be considered. The AVM library includes both pattern and resource module types. If your architecture includes or follows a well-known pattern then a pattern module may be the right option for you. If you determine this is the case, then search the module index for pattern modules in your chosen language to see if an option exists for your scenario. Otherwise, using resource modules from the library will be your best option.

In cases where an AVM resource or pattern module isn’t available for use, review the Bicep or Terraform provider documentation to identify how to augment AVM modules with standalone resources. If you feel that additional resource or pattern modules would be useful, you can also request the creation of a pattern or resource module by creating a module proposal issue on the AVM github repository.

Module source considerations

Once the decision has been made to use AVM modules to help accelerate solution development, a decision about where those modules will be sourced from is the next key decision point. A detailed exploration of the different sourcing options can be found in the Module Sourcing section of the Concepts page. Take a moment to review the options discussed there.

For our solution we will leverage the Public Registry option by sourcing AVM modules directly from the respective Terraform and Bicep public registries. This will avoid the need to fork copies of the modules for private use.

Subsections of Solution Development

Bicep - Solution Development

Introduction

Azure Verified Modules (AVM) for Bicep are a powerful tool that leverage the Bicep domain-specific language (DSL), industry knowledge, and an Open Source community, which altogether enable developers to quickly deploy Azure resources that follow Microsoft’s recommended practices for Azure.
In this article, we will walk through the Bicep specific considerations and recommended practices on developing your solution leveraging Azure Verified Modules. We’ll review some of the design features and trade-offs and include sample code to illustrate each discussion point.

In this tutorial, we will:

  • Deploy a basic Virtual Machine architecture into Azure
  • Explore recommended practices related to Bicep template development
  • Demonstrate the ease with which you can deploy AVM modules
  • Describe each of the development and deployment steps in detail

After completing this tutorial, you’ll have a working knowledge of:

  • How to discover and add AVM modules to your Bicep template
  • How to reference and use outputs across AVM modules
  • Recommended practices for parameterization and structure of your Bicep file
  • Configuration of AVM modules to meet Microsoft’s Well Architected Framework (WAF) principles
  • How to deploy your Bicep template into an Azure subscription from your local machine

Let’s get started!

Prerequisites

You will need the following tools and components to complete this guide:

Before you begin, make sure you have these tools installed in your development environment.

Solution Architecture

Before we begin coding, it is important to have details about what the infrastructure architecture will include. For our example, we will be building a solution that will host a simple application on a Linux virtual machine (VM). The solution must be secure and auditable. The VM must not be accessible from the internet and its logs should be easily accessible. All Azure services should utilize logging tools for auditing purposes.

Azure VM Solution Architecture

Develop the Solution Code

Creating the main.bicep file

The architecture diagram shows all components needed for a successful solution deployment. Rather than building the complete solution at once, this tutorial takes an incremental approach building the Bicep file piece-by-piece and testing the deployment at each stage. This approach allows for discussion of each design decision along the way.

The development will start with core platform components: first the backend logging services (Log Analytics) and then the virtual network.

Let’s begin by creating our folder structure along with a main.bicep file. Your folder structure should be as follows:

VirtualMachineAVM_Example1/
└── main.bicep

After you have your folder structure and main.bicep file, we can proceed with our first AVM resources!

Log Analytics

Let’s start by adding a logging service to our main.bicep since all other deployed resources will use this service for their logs.

Tip

Always begin template development by adding resources that create dependencies for other downstream services. This approach simplifies referencing these dependencies within your other modules as you develop them. For example, starting with Logging and Virtual Network services makes sense since all other services will depend on these.

The logging solution depicted in our Architecture Diagram shows we will be using a Log Analytics workspace. Let’s add that to our template! Open your main.bicep file and add the following:

βž• Expand Code
1module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
2  name: 'logAnalyticsWorkspace'
3  params: {
4    // Required parameters
5    name: 'VM-AVM-Ex1-law'
6    // Non-required parameters
7    location: 'westus2'
8  }
9}
Note

Always click on the “Copy to clipboard” button in the top right corner of the Code sample area in order not to have the line numbers included in the copied code.

You now have a fully functional Bicep template that will deploy a working Log Analytics workspace! If you would like to try it, run the following in your console:

Note

For keeping the example below simple, we are using the traditional deployment commands, e.g., az deployment group create or New-AzResourceGroupDeployment. However, we encourage you to look into using Deployment Stacks instead by simply replacing the previous commands with az stack group create or New-AzResourceGroupDeploymentStack as well as the other required input parameters as shown here.

Deployment Stacks allow you to deploy a Bicep file as a stack, which is a collection of resources that are deployed together. This allows you to manage the lifecycle of the stack as a single unit, making it easier to deploy, update, and now even delete resources via Bicep. You can also implement RBAC Deny Assignments on your stacks deployed resources to prevent changes to the resources or specific actions on the resources to all but an excluded list of users, groups or other principals.

Deploy with
# Log in to Azure
Connect-AzAccount

# Select your subscription
Set-AzContext -SubscriptionId '<subscriptionId>'

# Deploy a resource group
New-AzResourceGroup -Name 'avm-bicep-vmexample1' -Location '<location>'

# Invoke your deployment
New-AzResourceGroupDeployment -DeploymentName 'avm-bicep-vmexample1-deployment' -ResourceGroupName 'avm-bicep-vmexample1' -TemplateFile '/<path-to>/VirtualMachineAVM_Example1/main.bicep'
# Log in to Azure
az login

# Select your subscription
az account set --subscription '<subscriptionId>'

# Deploy a resource group
az group create --name 'avm-bicep-vmexample1' --location '<location>'

# Invoke your deployment
az deployment group create --name 'avm-bicep-vmexample1-deployment' --resource-group 'avm-bicep-vmexample1' --template-file '/<path-to>/VirtualMachineAVM_Example1/main.bicep'

The above commands will log you in to your Azure subscription, select a subscription to use, create a resource group, then deploy the main.bicep template to your resource group.

AVM Makes the deployment of Azure resources incredibly easy. Many of the parameters you would normally be required to define are taken care of by the AVM module itself. In fact, the location parameter is not even needed in your template—when left blank, by default, all AVM modules will deploy to the location in which your target Resource Group exists.

Now we have a Log Analytics workspace in our resource group which doesn’t do a whole lot of good on its own. Let’s take our template a step further by adding a Virtual Network that integrates with the Log Analytics workspace.

Virtual Network

We will now add a Virtual Network to our main.bicep file. This VNet will contain subnets and Network Security Groups (NSGs) for any of the resources we deploy that require IP addresses.

In your main.bicep file, add the following:

βž• Expand Code
 1module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 2  name: 'logAnalyticsWorkspace'
 3  params: {
 4    // Required parameters
 5    name: 'VM-AVM-Ex1-law'
 6    // Non-required parameters
 7    location: 'westus2'
 8  }
 9}
10
11module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
12  name: 'virtualNetworkDeployment'
13  params: {
14    // Required parameters
15    addressPrefixes: [
16      '10.0.0.0/16'
17    ]
18    name: 'VM-AVM-Ex1-vnet'
19    // Non-required parameters
20    location: 'westus2'
21  }
22}

Again, the Virtual Network AVM module requires only two things: a name and an addressPrefixes parameter.

Configure Diagnostics Settings

There is an additional parameter available in most AVM modules named diagnosticSettings. This parameter allows you to configure your resource to send its logs to any suitable logging service. In our case, we are using a Log Analytics workspace.

Let’s update our main.bicep file to have our VNet send all of its logging data to our Log Analytics workspace:

βž• Expand Code
 1module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 2  name: 'logAnalyticsWorkspace'
 3  params: {
 4    // Required parameters
 5    name: 'VM-AVM-Ex1-law'
 6    // Non-required parameters
 7    location: 'westus2'
 8  }
 9}
10
11module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
12  name: 'virtualNetworkDeployment'
13  params: {
14    // Required parameters
15    addressPrefixes: [
16      '10.0.0.0/16'
17    ]
18    name: 'VM-AVM-Ex1-vnet'
19    // Non-required parameters
20    location: 'westus2'
21    diagnosticSettings: [
22      {
23        name: 'vNetDiagnostics'
24        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
25      }
26    ]
27  }
28}

Notice how the diagnosticsSettings parameter needs a workspaceResourceId? All you need to do is add a reference to the built-in logAnalyticsWorkspaceId output of the logAnalyticsWorkspace AVM module. That’s it! Our VNet now has integrated its logging with our Log Analytics workspace. All AVM modules come with a set of built-in outputs that can be easily referenced by other modules within your template.

Info

All AVM modules have built-in outputs which can be referenced using the <moduleName>.outputs.<outputName> syntax.

When using plain Bicep, many of these outputs require multiple lines of code or knowledge of the correct object ID references to get at the desired output. AVM modules do much of this heavy lifting for you by taking care of these complex tasks within the module itself, then exposing them to you through the module’s outputs. Find out more about Bicep Outputs.

Add a Subnet and NAT Gateway

We can’t use a Virtual Network without subnets, so let’s add a subnet next. According to our Architecture, we will have three subnets: one for the Virtual Machine, one for the Bastion host, and one for Private Endpoints. We can start with the VM subnet for now. While we’re at it, let’s also add the NAT Gateway, the NAT Gateway’s Public IP, the attach the NAT Gateway to the VM subnet.

Add the following to your main.bicep:

βž• Expand Code
 1module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 2  name: 'logAnalyticsWorkspace'
 3  params: {
 4    // Required parameters
 5    name: 'VM-AVM-Ex1-law'
 6    // Non-required parameters
 7    location: 'westus2'
 8  }
 9}
10
11module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
12  name: 'natGwPublicIpDeployment'
13  params: {
14    // Required parameters
15    name: 'VM-AVM-Ex1-natgwpip'
16    // Non-required parameters
17    location: 'westus2'
18    diagnosticSettings: [
19      {
20        name: 'natGwPublicIpDiagnostics'
21        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
22      }
23    ]
24  }
25}
26
27module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
28  name: 'natGatewayDeployment'
29  params: {
30    // Required parameters
31    name: 'VM-AVM-Ex1-natGw'
32    zone: 1
33    // Non-required parameters
34    publicIpResourceIds: [
35      natGwPublicIp.outputs.resourceId
36    ]
37  }
38}
39
40module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
41  name: 'virtualNetworkDeployment'
42  params: {
43    // Required parameters
44    addressPrefixes: [
45      '10.0.0.0/16'
46    ]
47    name: 'VM-AVM-Ex1-vnet'
48    // Non-required parameters
49    location: 'westus2'
50    diagnosticSettings: [
51      {
52        name: 'vNetDiagnostics'
53        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
54      }
55    ]
56    subnets: [
57      {
58        name: 'VMSubnet'
59        addressPrefix: cidrSubnet('10.0.0.0/16', 24, 0) // first subnet in address space
60        natGatewayResourceId: natGateway.outputs.resourceId
61      }
62    ]
63  }
64}

The modification adds a subnets property to our virtualNetwork module. The AVM network/virtual-network module supports the creation of subnets directly within the module itself. We can also link our NAT Gateway directly to the subnet within this submodule.

A nice feature within Bicep are the various functions available. We use the cidrSubnet() function to declare CIDR blocks without having to calculate them on your own.

Switch to Parameters and Variables

See how we are reusing the same CIDR block 10.0.0.0/16 in multiple locations? You may have noticed we are defining the same location in two different spots as well. We’re now at a point in the development where we should leverage one of our first recommended practices: using parameters and variables!

Tip

Use Bicep variables to define values that will be constant and reused with your template; use parameters anywhere you may need a modifiable value.

Let’s enhance the template by adding variables for the CIDR block and prefix, then use a location parameter with a default value. We’ll then reference those in the module:

βž• Expand Code
 1param location string = 'westus2'
 2
 3var addressPrefix = '10.0.0.0/16'
 4var prefix = 'VM-AVM-Ex1'
 5
 6module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 7  name: 'logAnalyticsWorkspace'
 8  params: {
 9    // Required parameters
10    name: '${prefix}-law'
11    // Non-required parameters
12    location: location
13  }
14}
15
16module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
17  name: 'natGwPublicIpDeployment'
18  params: {
19    // Required parameters
20    name: '${prefix}-natgwpip'
21    // Non-required parameters
22    location: location
23    diagnosticSettings: [
24      {
25        name: 'natGwPublicIpDiagnostics'
26        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
27      }
28    ]
29  }
30}
31
32module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
33  name: 'natGatewayDeployment'
34  params: {
35    // Required parameters
36    name: '${prefix}-natgw'
37    zone: 1
38    // Non-required parameters
39    publicIpResourceIds: [
40      natGwPublicIp.outputs.resourceId
41    ]
42  }
43}
44
45module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
46  name: 'virtualNetworkDeployment'
47  params: {
48    // Required parameters
49    addressPrefixes: [
50      addressPrefix
51    ]
52    name: '${prefix}-vnet'
53    // Non-required parameters
54    location: location
55    diagnosticSettings: [
56      {
57        name: 'vNetDiagnostics'
58        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
59      }
60    ]
61    subnets: [
62      {
63        name: 'VMSubnet'
64        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
65        natGatewayResourceId: natGateway.outputs.resourceId
66      }
67    ]
68  }
69}

We now have a good basis for the infrastructure to be utilized by the rest of the resources in our Architecture. We will come back to our networking in a future step, once we are ready to create some Network Security Groups. For now, let’s move on to other modules.

Key Vault

Key Vaults are one of the key components in most Azure architectures as they create a place where you can save and reference secrets in a secure manner (“secrets” in the general sense, as opposed to the secret object type in Key Vaults). The Key Vault AVM module makes it very simple to store secrets generated in your template. In this tutorial, we will use one of the most secure methods of storing and retrieving secrets by leveraging this Key Vault in our Bicep template.

The first step is easy: add the Key Vault AVM module to our main.bicep file. In addition, let’s also ensure it’s hooked into our Log Analytics workspace (we will do this for every new module from here on out).

βž• Expand Code
 1param location string = 'westus2'
 2
 3var addressPrefix = '10.0.0.0/16'
 4var prefix = 'VM-AVM-Ex1'
 5
 6module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 7  name: 'logAnalyticsWorkspace'
 8  params: {
 9    // Required parameters
10    name: '${prefix}-law'
11    // Non-required parameters
12    location: location
13  }
14}
15
16module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
17  name: 'natGwPublicIpDeployment'
18  params: {
19    // Required parameters
20    name: '${prefix}-natgwpip'
21    // Non-required parameters
22    location: location
23    diagnosticSettings: [
24      {
25        name: 'natGwPublicIpDiagnostics'
26        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
27      }
28    ]
29  }
30}
31
32module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
33  name: 'natGatewayDeployment'
34  params: {
35    // Required parameters
36    name: '${prefix}-natgw'
37    zone: 1
38    // Non-required parameters
39    publicIpResourceIds: [
40      natGwPublicIp.outputs.resourceId
41    ]
42  }
43}
44
45module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
46  name: 'virtualNetworkDeployment'
47  params: {
48    // Required parameters
49    addressPrefixes: [
50      addressPrefix
51    ]
52    name: '${prefix}-vnet'
53    // Non-required parameters
54    location: location
55    diagnosticSettings: [
56      {
57        name: 'vNetDiagnostics'
58        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
59      }
60    ]
61    subnets: [
62      {
63        name: 'VMSubnet'
64        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
65        natGatewayResourceId: natGateway.outputs.resourceId
66      }
67    ]
68  }
69}
70
71module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
72  name: 'keyVaultDeployment'
73  params: {
74    // Required parameters
75    name: '${uniqueString(resourceGroup().id)}-kv'
76    // Non-required parameters
77    location: location
78    diagnosticSettings: [
79      {
80        name: 'keyVaultDiagnostics'
81        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
82      }
83    ]
84  }
85}

The name of the Key Vault we will deploy uses the uniqueString() Bicep function. Key Vault names must be globally unique. We will therefore deviate from our standard naming convention thus far and make an exception for the Key Vault. Note how we are still adding a suffix to the Key Vault name, so its name remains recognizable; you can use a combination of concatenating unique strings, prefixes, or suffixes to follow your own naming standard preferences.

When we generate our unique string, we will pass in the resourceGroup().id as the seed for the uniqueString() function so that every time you deploy this main.bicep to the same resource group, it will use the same randomly generated name for your Key Vault (since resourceGroup().id will be the same).

Tip

Bicep has many built-in functions available. We used two here: uniqueString() and resourceGroup(). The resourceGroup(), subscription(), and deployment() functions are very useful when seeding uniqueString() or guid() functions. Just be cautious about name length limitations for each Azure service! Visit this page to learn more about Bicep functions.

We will use this Key Vault later on when we create a VM and need to store its password. Now that we have it, a Virtual Network, Subnet, and Log Analytics prepared, we should have everything we need to deploy a Virtual Machine!

Info

In the future, we will update this guide to show how to generate and store a certificate in the Key Vault, then use that certificate to authenticate into the Virtual Machine.

Virtual Machine

Warning

The AVM Virtual Machine module enables the EncryptionAtHost feature by default. You must enable this feature within your Azure subscription successfully deploy this example code. To do so, run the following:

Deploy with
# Wait a few minutes after running the command to allow it to propagate
Register-AzProviderFeature -FeatureName "EncryptionAtHost" -ProviderNamespace "Microsoft.Compute"
az feature register --namespace Microsoft.Compute --name EncryptionAtHost

# Propagate the change
az provider register --namespace Microsoft.Compute

For our Virtual Machine (VM) deployment, we need to add the following to our main.bicep file:

βž• Expand Code
  1param location string = 'westus2'
  2
  3// START add-password-param
  4@description('Required. A password for the VM admin user.')
  5@secure()
  6param vmAdminPass string
  7// END add-password-param
  8
  9var addressPrefix = '10.0.0.0/16'
 10var prefix = 'VM-AVM-Ex1'
 11
 12module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 13  name: 'logAnalyticsWorkspace'
 14  params: {
 15    // Required parameters
 16    name: '${prefix}-law'
 17    // Non-required parameters
 18    location: location
 19  }
 20}
 21
 22module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 23  name: 'natGwPublicIpDeployment'
 24  params: {
 25    // Required parameters
 26    name: '${prefix}-natgwpip'
 27    // Non-required parameters
 28    location: location
 29    diagnosticSettings: [
 30      {
 31        name: 'natGwPublicIpDiagnostics'
 32        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 33      }
 34    ]
 35  }
 36}
 37
 38module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 39  name: 'natGatewayDeployment'
 40  params: {
 41    // Required parameters
 42    name: '${prefix}-natgw'
 43    zone: 1
 44    // Non-required parameters
 45    publicIpResourceIds: [
 46      natGwPublicIp.outputs.resourceId
 47    ]
 48  }
 49}
 50
 51module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 52  name: 'virtualNetworkDeployment'
 53  params: {
 54    // Required parameters
 55    addressPrefixes: [
 56      addressPrefix
 57    ]
 58    name: '${prefix}-vnet'
 59    // Non-required parameters
 60    location: location
 61    diagnosticSettings: [
 62      {
 63        name: 'vNetDiagnostics'
 64        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 65      }
 66    ]
 67    subnets: [
 68      {
 69        name: 'VMSubnet'
 70        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 71        natGatewayResourceId: natGateway.outputs.resourceId
 72      }
 73    ]
 74  }
 75}
 76
 77module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
 78  name: 'keyVaultDeployment'
 79  params: {
 80    // Required parameters
 81    name: '${uniqueString(resourceGroup().id)}-kv'
 82    // Non-required parameters
 83    location: location
 84    diagnosticSettings: [
 85      {
 86        name: 'keyVaultDiagnostics'
 87        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 88      }
 89    ]
 90    // START add-keyvault-secret
 91    secrets: [
 92      {
 93        name: 'vmAdminPassword'
 94        value: vmAdminPass
 95      }
 96    ]
 97    // END add-keyvault-secret
 98  }
 99}
100
101module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.1' = {
102  name: 'linuxVirtualMachineDeployment'
103  params: {
104    // Required parameters
105    adminUsername: 'localAdminUser'
106    adminPassword: vmAdminPass
107    imageReference: {
108      offer: '0001-com-ubuntu-server-jammy'
109      publisher: 'Canonical'
110      sku: '22_04-lts-gen2'
111      version: 'latest'
112    }
113    name: '${prefix}-vm1'
114    // START vm-subnet-reference
115    nicConfigurations: [
116      {
117        ipConfigurations: [
118          {
119            name: 'ipconfig01'
120            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
121          }
122        ]
123        nicSuffix: '-nic-01'
124      }
125    ]
126    // END vm-subnet-reference
127    osDisk: {
128      caching: 'ReadWrite'
129      diskSizeGB: 128
130      managedDisk: {
131        storageAccountType: 'Standard_LRS'
132      }
133    }
134    osType: 'Linux'
135    vmSize: 'Standard_B2s_v2'
136    zone: 0
137    // Non-required parameters
138    location: location
139  }
140}

The VM module is one of the more complex modules in AVM—behind the scenes, it takes care of a lot of heavy lifting that, without AVM, would require multiple Bicep resources to be deployed and referenced.

For example, look at the nicConfigurations parameter: normally, you would need to deploy a separate NIC resource, which itself also requires an IP resource, then attach them to each other, and finally, attach them all to your VM.

With the AVM VM module, the nicConfigurations parameter accepts an object, allowing you to create any number of NICs to attach to your VM from within the VM resource deployment itself. It handles all the naming, creation of other necessary dependencies, and attaches them all together, so you don’t have to. The osDisk parameter is similar, though slightly less complex. There are many more parameters within the VM module that you can leverage if needed, that share a similar ease-of-use.

Since this is the real highlight of our main.bicep file, we need to take a closer look at some of the other changes that were made.

  • VM Admin Password Parameter

    1@description('Required. A password for the VM admin user.')
    2@secure()
    3param vmAdminPass string

    First, we added a new parameter. The value of this will be provided when the main.bicep template is deployed. We don’t want any passwords stored as text in code; for our purposes, the safest way to do this is to prompt the end user for the password at the time of deployment.

    Warning

    The supplied password must be between 6-72 characters long and must satisfy at least 3 of password complexity requirements from the following: Contains an uppercase character; Contains a lowercase character; Contains a numeric digit; Contains a special character. Control characters are not allowed

    Also note how we are using the @secure() decorator on the password parameter. This will ensure the value of the password is never displayed in any of the deployment logs or in Azure. We have also added the @description() decorator and started the description with “Required.” It’s a good habit and recommended practice to document your parameters in Bicep. This will ensure that VS Code’s built-in Bicep linter can provide end-users insightful information when deploying your Bicep templates.

    Info

    Always use the @secure() decorator when creating a parameter that will hold sensitive data!

  • Add the VM Admin Password to Key Vault

    1    secrets: [
    2      {
    3        name: 'vmAdminPassword'
    4        value: vmAdminPass
    5      }
    6    ]

    The next thing we have done is save the value of our vmAdminPass parameter to our Key Vault. We have done this by adding a secrets parameter to the Key Vault module. Adding secrets to Key Vaults is very simple when using the AVM module.

    By adding our password to the Key Vault, it will ensure that we never lose the password and that it is stored securely. As long as a user has appropriate permissions on the vault, the password can be fetched easily.

  • Reference the VM Subnet

     1    nicConfigurations: [
     2      {
     3        ipConfigurations: [
     4          {
     5            name: 'ipconfig01'
     6            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
     7          }
     8        ]
     9        nicSuffix: '-nic-01'
    10      }
    11    ]

    Here, we reference another built-in output, this time from the AVM Virtual Network module. This example shows how to use an output that is part of an array. When the Virtual Network module creates subnets, it automatically creates a set of pre-defined outputs for them, one of which is an array that contains each subnet’s subnetResourceId. Our VM Subnet was the first one created which is position [0] in the array.

    Other AVM modules may make use of arrays to store outputs. If you are unsure what type of outputs a module provides, you can always reference the Outputs section of each module’s README.md.

Storage Account

The last major component we need to add is a Storage Account. Because this Storage Account will be used as a backend storage to hold blobs for the hypothetical application that runs on our VM, we’ll also create a blob container within it using the same AVM Storage Account module.

βž• Expand Code
  1param location string = 'westus2'
  2
  3@description('Required. A password for the VM admin user.')
  4@secure()
  5param vmAdminPass string
  6
  7var addressPrefix = '10.0.0.0/16'
  8var prefix = 'VM-AVM-Ex1'
  9
 10module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 11  name: 'logAnalyticsWorkspace'
 12  params: {
 13    // Required parameters
 14    name: '${prefix}-law'
 15    // Non-required parameters
 16    location: location
 17  }
 18}
 19
 20module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 21  name: 'natGwPublicIpDeployment'
 22  params: {
 23    // Required parameters
 24    name: '${prefix}-natgwpip'
 25    // Non-required parameters
 26    location: location
 27    diagnosticSettings: [
 28      {
 29        name: 'natGwPublicIpDiagnostics'
 30        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 31      }
 32    ]
 33  }
 34}
 35
 36module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 37  name: 'natGatewayDeployment'
 38  params: {
 39    // Required parameters
 40    name: '${prefix}-natgw'
 41    zone: 1
 42    // Non-required parameters
 43    publicIpResourceIds: [
 44      natGwPublicIp.outputs.resourceId
 45    ]
 46  }
 47}
 48
 49module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 50  name: 'virtualNetworkDeployment'
 51  params: {
 52    // Required parameters
 53    addressPrefixes: [
 54      addressPrefix
 55    ]
 56    name: '${prefix}-vnet'
 57    // Non-required parameters
 58    location: location
 59    diagnosticSettings: [
 60      {
 61
 62        name: 'vNetDiagnostics'
 63        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 64      }
 65    ]
 66    subnets: [
 67      {
 68        name: 'VMSubnet'
 69        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 70        natGatewayResourceId: natGateway.outputs.resourceId
 71      }
 72    ]
 73  }
 74}
 75
 76module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
 77  name: 'keyVaultDeployment'
 78  params: {
 79    // Required parameters
 80    name: '${uniqueString(resourceGroup().id)}-kv'
 81    // Non-required parameters
 82    location: location
 83    diagnosticSettings: [
 84      {
 85        name: 'keyVaultDiagnostics'
 86        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 87      }
 88    ]
 89    enablePurgeProtection: false // disable purge protection for this example so we can more easily delete it
 90    secrets: [
 91      {
 92        name: 'vmAdminPassword'
 93        value: vmAdminPass
 94      }
 95    ]
 96  }
 97}
 98
 99module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.1' = {
100  name: 'linuxVirtualMachineDeployment'
101  params: {
102    // Required parameters
103    adminUsername: 'localAdminUser'
104    adminPassword: vmAdminPass
105    imageReference: {
106      offer: '0001-com-ubuntu-server-jammy'
107      publisher: 'Canonical'
108      sku: '22_04-lts-gen2'
109      version: 'latest'
110    }
111    name: '${prefix}-vm1'
112    nicConfigurations: [
113      {
114        ipConfigurations: [
115          {
116            name: 'ipconfig01'
117            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
118          }
119        ]
120        nicSuffix: '-nic-01'
121      }
122    ]
123    osDisk: {
124      caching: 'ReadWrite'
125      diskSizeGB: 128
126      managedDisk: {
127        storageAccountType: 'Standard_LRS'
128      }
129    }
130
131    osType: 'Linux'
132    vmSize: 'Standard_B2s_v2'
133    zone: 0
134    // Non-required parameters
135    location: location
136  }
137}
138
139module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
140  name: 'storageAccountDeployment'
141  params: {
142    // Required parameters
143    name: '${uniqueString(resourceGroup().id)}sa'
144    // Non-required parameters
145    location: location
146    skuName: 'Standard_LRS'
147    diagnosticSettings: [
148      {
149        name: 'storageAccountDiagnostics'
150        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
151      }
152    ]
153    blobServices: {
154      containers: [
155        {
156          name: 'vmstorage'
157          publicAccess: 'None'
158        }
159      ]
160    }
161  }
162}

We now have all the major components of our Architecture diagram built!

The last steps we need to take to meet our requirements is to ensure our networking resources are secure and that we are using least privileged access by leveraging Role-Based Access Control (RBAC). Let’s get to it!

Network Security Groups

We’ll add a Network Security Group (NSG) to our VM subnet. This will act as a layer 3 and layer 4 firewall for networked resources. This implementation includes an appropriate inbound rule to allow SSH traffic from the Bastion host:

βž• Expand Code
  1param location string = 'westus2'
  2
  3@description('Required. A password for the VM admin user.')
  4@secure()
  5param vmAdminPass string
  6
  7var addressPrefix = '10.0.0.0/16'
  8var prefix = 'VM-AVM-Ex1'
  9
 10module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 11  name: 'logAnalyticsWorkspace'
 12  params: {
 13    // Required parameters
 14    name: '${prefix}-law'
 15    // Non-required parameters
 16    location: location
 17  }
 18}
 19
 20module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 21  name: 'natGwPublicIpDeployment'
 22  params: {
 23    // Required parameters
 24    name: '${prefix}-natgwpip'
 25    // Non-required parameters
 26    location: location
 27    diagnosticSettings: [
 28      {
 29        name: 'natGwPublicIpDiagnostics'
 30        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 31      }
 32    ]
 33  }
 34}
 35
 36module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 37  name: 'natGatewayDeployment'
 38  params: {
 39    // Required parameters
 40    name: '${prefix}-natgw'
 41    zone: 1
 42    // Non-required parameters
 43    publicIpResourceIds: [
 44      natGwPublicIp.outputs.resourceId
 45    ]
 46  }
 47}
 48
 49module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 50  name: 'virtualNetworkDeployment'
 51  params: {
 52    // Required parameters
 53    addressPrefixes: [
 54      addressPrefix
 55    ]
 56    name: '${prefix}-vnet'
 57    // Non-required parameters
 58    location: location
 59    diagnosticSettings: [
 60      {
 61        name: 'vNetDiagnostics'
 62        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 63      }
 64    ]
 65    subnets: [
 66      {
 67        name: 'VMSubnet'
 68        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 69        natGatewayResourceId: natGateway.outputs.resourceId
 70        networkSecurityGroupResourceId: nsgVM.outputs.resourceId
 71      }
 72    ]
 73  }
 74}
 75
 76module nsgVM 'br/public:avm/res/network/network-security-group:0.5.1' = {
 77  name: 'nsgVmDeployment'
 78  params: {
 79    name: '${prefix}-NSG-VM'
 80    location: location
 81    securityRules: [
 82      {
 83        name: 'AllowBastionSSH'
 84        properties: {
 85          access: 'Allow'
 86          direction: 'Inbound'
 87          priority: 100
 88          protocol: 'Tcp'
 89          sourceAddressPrefix: 'virtualNetwork'
 90          sourcePortRange: '*'
 91          destinationAddressPrefix: '*'
 92          destinationPortRange: '22'
 93        }
 94      }
 95    ]
 96  }
 97}
 98
 99module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
100  name: 'keyVaultDeployment'
101  params: {
102    // Required parameters
103    name: '${uniqueString(resourceGroup().id)}-kv'
104    // Non-required parameters
105    location: location
106    diagnosticSettings: [
107      {
108        name: 'keyVaultDiagnostics'
109        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
110      }
111    ]
112    enablePurgeProtection: false // disable purge protection for this example so we can more easily delete it
113    secrets: [
114      {
115        name: 'vmAdminPassword'
116        value: vmAdminPass
117      }
118    ]
119  }
120}
121
122module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.13.1' = {
123  name: 'linuxVirtualMachineDeployment'
124  params: {
125    // Required parameters
126    adminUsername: 'localAdminUser'
127    adminPassword: vmAdminPass
128    imageReference: {
129      offer: '0001-com-ubuntu-server-jammy'
130      publisher: 'Canonical'
131      sku: '22_04-lts-gen2'
132      version: 'latest'
133    }
134    name: '${prefix}-vm1'
135    nicConfigurations: [
136      {
137        ipConfigurations: [
138          {
139            name: 'ipconfig01'
140            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
141          }
142        ]
143        nicSuffix: '-nic-01'
144      }
145    ]
146    osDisk: {
147      caching: 'ReadWrite'
148      diskSizeGB: 128
149      managedDisk: {
150        storageAccountType: 'Standard_LRS'
151      }
152    }
153
154    osType: 'Linux'
155    vmSize: 'Standard_B2s_v2'
156    zone: 0
157    // Non-required parameters
158    location: location
159  }
160}
161
162module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
163  name: 'storageAccountDeployment'
164  params: {
165    // Required parameters
166    name: '${uniqueString(resourceGroup().id)}sa'
167    // Non-required parameters
168    location: location
169    skuName: 'Standard_LRS'
170    diagnosticSettings: [
171      {
172        name: 'storageAccountDiagnostics'
173        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
174      }
175    ]
176    blobServices: {
177      containers: [
178        {
179          name: 'vmstorage'
180          publicAccess: 'None'
181        }
182      ]
183    }
184  }
185}

Disable Public Access to Storage Account

Since the Storage Account serves as a backend resource exclusively for the Virtual Machine, it will be secured as much as possible. This involves adding a Private Endpoint and disabling public internet access. AVM makes creation and assignment of Private Endpoints to resources incredibly easy. Take a look:

βž• Expand Code
  1param location string = 'westus2'
  2
  3@description('Required. A password for the VM admin user.')
  4@secure()
  5param vmAdminPass string
  6
  7var addressPrefix = '10.0.0.0/16'
  8var prefix = 'VM-AVM-Ex1'
  9
 10module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 11  name: 'logAnalyticsWorkspace'
 12  params: {
 13    // Required parameters
 14    name: '${prefix}-law'
 15    // Non-required parameters
 16    location: location
 17  }
 18}
 19
 20module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 21  name: 'natGwPublicIpDeployment'
 22  params: {
 23    // Required parameters
 24    name: '${prefix}-natgwpip'
 25    // Non-required parameters
 26    location: location
 27    diagnosticSettings: [
 28      {
 29        name: 'natGwPublicIpDiagnostics'
 30        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 31      }
 32    ]
 33  }
 34}
 35
 36module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 37  name: 'natGatewayDeployment'
 38  params: {
 39    // Required parameters
 40    name: '${prefix}-natgw'
 41    zone: 1
 42    // Non-required parameters
 43    publicIpResourceIds: [
 44      natGwPublicIp.outputs.resourceId
 45    ]
 46  }
 47}
 48
 49module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 50  name: 'virtualNetworkDeployment'
 51  params: {
 52    // Required parameters
 53    addressPrefixes: [
 54      addressPrefix
 55    ]
 56    name: '${prefix}-vnet'
 57    // Non-required parameters
 58    location: location
 59    diagnosticSettings: [
 60      {
 61        name: 'vNetDiagnostics'
 62        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 63      }
 64    ]
 65    subnets: [
 66      {
 67        name: 'VMSubnet'
 68        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 69        natGatewayResourceId: natGateway.outputs.resourceId
 70        networkSecurityGroupResourceId: nsgVM.outputs.resourceId
 71      }
 72      {
 73        name: 'PrivateEndpointSubnet'
 74        addressPrefix: cidrSubnet(addressPrefix, 24, 1) // second subnet in address space
 75      }
 76    ]
 77  }
 78}
 79
 80module nsgVM 'br/public:avm/res/network/network-security-group:0.5.1' = {
 81  name: 'nsgVmDeployment'
 82  params: {
 83    name: '${prefix}-NSG-VM'
 84    location: location
 85    securityRules: [
 86      {
 87        name: 'AllowBastionSSH'
 88        properties: {
 89          access: 'Allow'
 90          direction: 'Inbound'
 91          priority: 100
 92          protocol: 'Tcp'
 93          sourceAddressPrefix: 'virtualNetwork'
 94          sourcePortRange: '*'
 95          destinationAddressPrefix: '*'
 96          destinationPortRange: '22'
 97        }
 98      }
 99    ]
100  }
101}
102
103module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
104  name: 'keyVaultDeployment'
105  params: {
106    // Required parameters
107    name: '${uniqueString(resourceGroup().id)}-kv'
108    // Non-required parameters
109    location: location
110    diagnosticSettings: [
111      {
112        name: 'keyVaultDiagnostics'
113        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
114      }
115    ]
116    enablePurgeProtection: false // disable purge protection for this example so we can more easily delete it
117    secrets: [
118      {
119        name: 'vmAdminPassword'
120        value: vmAdminPass
121      }
122    ]
123  }
124}
125
126module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.14.0' = {
127  name: 'linuxVirtualMachineDeployment'
128  params: {
129    // Required parameters
130    adminUsername: 'localAdminUser'
131    adminPassword: vmAdminPass
132    imageReference: {
133      offer: '0001-com-ubuntu-server-jammy'
134      publisher: 'Canonical'
135      sku: '22_04-lts-gen2'
136      version: 'latest'
137    }
138    name: '${prefix}-vm1'
139    nicConfigurations: [
140      {
141        ipConfigurations: [
142          {
143            name: 'ipconfig01'
144            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
145          }
146        ]
147        nicSuffix: '-nic-01'
148      }
149    ]
150    osDisk: {
151      caching: 'ReadWrite'
152      diskSizeGB: 128
153      managedDisk: {
154        storageAccountType: 'Standard_LRS'
155      }
156    }
157    osType: 'Linux'
158    vmSize: 'Standard_B2s_v2'
159    zone: 0
160    // Non-required parameters
161    location: location
162  }
163}
164
165module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
166  name: 'storageAccountDeployment'
167  params: {
168    // Required parameters
169    name: '${uniqueString(resourceGroup().id)}sa'
170    // Non-required parameters
171    location: location
172    skuName: 'Standard_LRS'
173    diagnosticSettings: [
174      {
175        name: 'storageAccountDiagnostics'
176        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
177      }
178    ]
179    publicNetworkAccess: 'Disabled'
180    allowBlobPublicAccess: false
181    blobServices: {
182      containers: [
183        {
184          name: 'vmstorage'
185          publicAccess: 'None'
186        }
187      ]
188    }
189    privateEndpoints: [
190      {
191        service: 'Blob'
192        subnetResourceId: virtualNetwork.outputs.subnetResourceIds[1] // Private Endpoint Subnet
193        privateDnsZoneGroup: {
194          privateDnsZoneGroupConfigs: [
195            {
196              privateDnsZoneResourceId: privateDnsBlob.outputs.resourceId
197            }
198          ]
199        }
200      }
201    ]
202  }
203}
204
205module privateDnsBlob 'br/public:avm/res/network/private-dns-zone:0.7.1' = {
206  name: '${prefix}-privatedns-blob'
207  params: {
208    name: 'privatelink.blob.${environment().suffixes.storage}'
209    location: 'global'
210    virtualNetworkLinks: [
211      {
212        name: '${virtualNetwork.outputs.name}-vnetlink'
213        virtualNetworkResourceId: virtualNetwork.outputs.resourceId
214      }
215    ]
216  }
217}

This implementation adds a dedicated subnet for Private Endpoints following the recommended practice of isolating Private Endpoints in their own subnet.

The addition of just a few lines of code in the privateEndpoints parameter handles the complex tasks of creating the Private Endpoint, associating it with the VNet, and attaching it to the resource. AVM drastically simplifies the creation of Private Endpoints for just about every Azure Resource that supports them.

The implementation also disables all public network connectivity to the Storage Account, ensuring it only accepts traffic via the Private Endpoint.

Finally, a Private DNS zone is added and linked to the VNet, enabling the VM to resolve the Private IP address associated with the Storage Account.

Bastion

To securely access the Virtual Machine without exposing its SSH port to the public internet, we’ll create an Azure Bastion host. The Bastion Host requires a subnet with the exact name AzureBastionSubnet which cannot contain anything other than Bastion Hosts.

βž• Expand Code
  1param location string = 'westus2'
  2
  3@description('Required. A password for the VM admin user.')
  4@secure()
  5param vmAdminPass string
  6
  7var addressPrefix = '10.0.0.0/16'
  8var prefix = 'VM-AVM-Ex1'
  9
 10module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 11  name: 'logAnalyticsWorkspace'
 12  params: {
 13    // Required parameters
 14    name: '${prefix}-law'
 15    // Non-required parameters
 16    location: location
 17  }
 18}
 19
 20module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 21  name: 'natGwPublicIpDeployment'
 22  params: {
 23    // Required parameters
 24    name: '${prefix}-natgwpip'
 25    // Non-required parameters
 26    location: location
 27    diagnosticSettings: [
 28      {
 29        name: 'natGwPublicIpDiagnostics'
 30        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 31      }
 32    ]
 33  }
 34}
 35
 36module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 37  name: 'natGatewayDeployment'
 38  params: {
 39    // Required parameters
 40    name: '${prefix}-natgw'
 41    zone: 1
 42    // Non-required parameters
 43    publicIpResourceIds: [
 44      natGwPublicIp.outputs.resourceId
 45    ]
 46  }
 47}
 48
 49module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 50  name: 'virtualNetworkDeployment'
 51  params: {
 52    // Required parameters
 53    addressPrefixes: [
 54      addressPrefix
 55    ]
 56    name: '${prefix}-vnet'
 57    // Non-required parameters
 58    location: location
 59    diagnosticSettings: [
 60      {
 61        name: 'vNetDiagnostics'
 62        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 63      }
 64    ]
 65    subnets: [
 66      {
 67        name: 'VMSubnet'
 68        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 69        natGatewayResourceId: natGateway.outputs.resourceId
 70        networkSecurityGroupResourceId: nsgVM.outputs.resourceId
 71      }
 72      {
 73        name: 'PrivateEndpointSubnet'
 74        addressPrefix: cidrSubnet(addressPrefix, 24, 1) // second subnet in address space
 75      }
 76      {
 77        name: 'AzureBastionSubnet' // Azure Bastion Host requires this subnet to be named exactly "AzureBastionSubnet"
 78        addressPrefix: cidrSubnet(addressPrefix, 24, 2) // third subnet in address space
 79      }
 80    ]
 81  }
 82}
 83
 84module nsgVM 'br/public:avm/res/network/network-security-group:0.5.1' = {
 85  name: 'nsgVmDeployment'
 86  params: {
 87    name: '${prefix}-NSG-VM'
 88    location: location
 89    securityRules: [
 90      {
 91        name: 'AllowBastionSSH'
 92        properties: {
 93          access: 'Allow'
 94          direction: 'Inbound'
 95          priority: 100
 96          protocol: 'Tcp'
 97          sourceAddressPrefix: 'virtualNetwork'
 98          sourcePortRange: '*'
 99          destinationAddressPrefix: '*'
100          destinationPortRange: '22'
101        }
102      }
103    ]
104  }
105}
106
107module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
108  name: 'keyVaultDeployment'
109  params: {
110    // Required parameters
111    name: '${uniqueString(resourceGroup().id)}-kv'
112    // Non-required parameters
113    location: location
114    diagnosticSettings: [
115      {
116        name: 'keyVaultDiagnostics'
117        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
118      }
119    ]
120    enablePurgeProtection: false // disable purge protection for this example so we can more easily delete it
121    secrets: [
122      {
123        name: 'vmAdminPassword'
124        value: vmAdminPass
125      }
126    ]
127  }
128}
129
130module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.14.0' = {
131  name: 'linuxVirtualMachineDeployment'
132  params: {
133    // Required parameters
134    adminUsername: 'localAdminUser'
135    adminPassword: vmAdminPass
136    imageReference: {
137      offer: '0001-com-ubuntu-server-jammy'
138      publisher: 'Canonical'
139      sku: '22_04-lts-gen2'
140      version: 'latest'
141    }
142    name: '${prefix}-vm1'
143    nicConfigurations: [
144      {
145        ipConfigurations: [
146          {
147            name: 'ipconfig01'
148            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
149          }
150        ]
151        nicSuffix: '-nic-01'
152      }
153    ]
154    osDisk: {
155      caching: 'ReadWrite'
156      diskSizeGB: 128
157      managedDisk: {
158        storageAccountType: 'Standard_LRS'
159      }
160    }
161    osType: 'Linux'
162    vmSize: 'Standard_B2s_v2'
163    zone: 0
164    // Non-required parameters
165    location: location
166  }
167}
168
169module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
170  name: 'storageAccountDeployment'
171  params: {
172    // Required parameters
173    name: '${uniqueString(resourceGroup().id)}sa'
174    // Non-required parameters
175    location: location
176    skuName: 'Standard_LRS'
177    diagnosticSettings: [
178      {
179        name: 'storageAccountDiagnostics'
180        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
181      }
182    ]
183    publicNetworkAccess: 'Disabled'
184    allowBlobPublicAccess: false
185    blobServices: {
186      containers: [
187        {
188          name: 'vmstorage'
189          publicAccess: 'None'
190        }
191      ]
192    }
193    privateEndpoints: [
194      {
195        service: 'Blob'
196        subnetResourceId: virtualNetwork.outputs.subnetResourceIds[1] // Private Endpoint Subnet
197        privateDnsZoneGroup: {
198          privateDnsZoneGroupConfigs: [
199            {
200              privateDnsZoneResourceId: privateDnsBlob.outputs.resourceId
201            }
202          ]
203        }
204      }
205    ]
206  }
207}
208
209module privateDnsBlob 'br/public:avm/res/network/private-dns-zone:0.7.1' = {
210  name: '${prefix}-privatedns-blob'
211  params: {
212    name: 'privatelink.blob.${environment().suffixes.storage}'
213    location: 'global'
214    virtualNetworkLinks: [
215      {
216        name: '${virtualNetwork.outputs.name}-vnetlink'
217        virtualNetworkResourceId: virtualNetwork.outputs.resourceId
218      }
219    ]
220  }
221}
222
223// Note: Deploying a Bastion Host will automatically create a Public IP and use the subnet named "AzureBastionSubnet"
224// within our VNet. This subnet is required and must be named exactly "AzureBastionSubnet" for the Bastion Host to work.
225module bastion 'br/public:avm/res/network/bastion-host:0.6.1' = {
226  name: 'bastionDeployment'
227  params: {
228    name: '${prefix}-bastion'
229    virtualNetworkResourceId: virtualNetwork.outputs.resourceId
230    skuName: 'Basic'
231    location: location
232    diagnosticSettings: [
233      {
234        name: 'bastionDiagnostics'
235        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
236      }
237    ]
238  }
239}

This simple addition of the bastion-host AVM module completes the secure access component of our architecture. You can now access the Virtual Machine by way of the Bastion Host in the Azure Portal.

Role-Based Access Control

To complete our solution, we have one final task: to apply Role-Based Access Control (RBAC) restrictions on our services, namely the Key Vault and Storage Account. The goal is to explicitly allow only the Virtual Machine to have Create, Read, Update, or Delete (CRUD) permissions on these two services.

This is accomplished by enabling a System-assigned Managed Identity on the Virtual Machine, then granting the VM’s Managed Identity appropriate permissions on the Storage Account and Key Vault:

βž• Expand Code
  1param location string = 'westus2'
  2
  3@description('Required. A password for the VM admin user.')
  4@secure()
  5param vmAdminPass string
  6
  7var addressPrefix = '10.0.0.0/16'
  8var prefix = 'VM-AVM-Ex1'
  9
 10module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 11  name: 'logAnalyticsWorkspace'
 12  params: {
 13    // Required parameters
 14    name: '${prefix}-law'
 15    // Non-required parameters
 16    location: location
 17  }
 18}
 19
 20module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 21  name: 'natGwPublicIpDeployment'
 22  params: {
 23    // Required parameters
 24    name: '${prefix}-natgwpip'
 25    // Non-required parameters
 26    location: location
 27    diagnosticSettings: [
 28      {
 29        name: 'natGwPublicIpDiagnostics'
 30        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 31      }
 32    ]
 33  }
 34}
 35
 36module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 37  name: 'natGatewayDeployment'
 38  params: {
 39    // Required parameters
 40    name: '${prefix}-natgw'
 41    zone: 1
 42    // Non-required parameters
 43    publicIpResourceIds: [
 44      natGwPublicIp.outputs.resourceId
 45    ]
 46  }
 47}
 48
 49module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 50  name: 'virtualNetworkDeployment'
 51  params: {
 52    // Required parameters
 53    addressPrefixes: [
 54      addressPrefix
 55    ]
 56    name: '${prefix}-vnet'
 57    // Non-required parameters
 58    location: location
 59    diagnosticSettings: [
 60      {
 61        name: 'vNetDiagnostics'
 62        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 63      }
 64    ]
 65    subnets: [
 66      {
 67        name: 'VMSubnet'
 68        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 69        natGatewayResourceId: natGateway.outputs.resourceId
 70        networkSecurityGroupResourceId: nsgVM.outputs.resourceId
 71      }
 72      {
 73        name: 'PrivateEndpointSubnet'
 74        addressPrefix: cidrSubnet(addressPrefix, 24, 1) // second subnet in address space
 75      }
 76      {
 77        name: 'AzureBastionSubnet' // Azure Bastion Host requires this subnet to be named exactly "AzureBastionSubnet"
 78        addressPrefix: cidrSubnet(addressPrefix, 24, 2) // third subnet in address space
 79      }
 80    ]
 81  }
 82}
 83
 84module nsgVM 'br/public:avm/res/network/network-security-group:0.5.1' = {
 85  name: 'nsgVmDeployment'
 86  params: {
 87    name: '${prefix}-NSG-VM'
 88    location: location
 89    securityRules: [
 90      {
 91        name: 'AllowBastionSSH'
 92        properties: {
 93          access: 'Allow'
 94          direction: 'Inbound'
 95          priority: 100
 96          protocol: 'Tcp'
 97          sourceAddressPrefix: 'virtualNetwork'
 98          sourcePortRange: '*'
 99          destinationAddressPrefix: '*'
100          destinationPortRange: '22'
101        }
102      }
103    ]
104  }
105}
106
107module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
108  name: 'keyVaultDeployment'
109  params: {
110    // Required parameters
111    name: '${uniqueString(resourceGroup().id)}-kv'
112    // Non-required parameters
113    location: location
114    diagnosticSettings: [
115      {
116        name: 'keyVaultDiagnostics'
117        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
118      }
119    ]
120    enablePurgeProtection: false // disable purge protection for this example so we can more easily delete it
121    secrets: [
122      {
123        name: 'vmAdminPassword'
124        value: vmAdminPass
125      }
126    ]
127    roleAssignments: [
128      {
129        principalId: virtualMachine.outputs.systemAssignedMIPrincipalId
130        principalType: 'ServicePrincipal'
131        roleDefinitionIdOrName: 'Key Vault Secrets User' // Allows read access to secrets
132      }
133    ]
134  }
135}
136
137module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.14.0' = {
138  name: 'linuxVirtualMachineDeployment'
139  params: {
140    // Required parameters
141    adminUsername: 'localAdminUser'
142    adminPassword: vmAdminPass
143    imageReference: {
144      offer: '0001-com-ubuntu-server-jammy'
145      publisher: 'Canonical'
146      sku: '22_04-lts-gen2'
147      version: 'latest'
148    }
149    name: '${prefix}-vm1'
150    nicConfigurations: [
151      {
152        ipConfigurations: [
153          {
154            name: 'ipconfig01'
155            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
156          }
157        ]
158        nicSuffix: '-nic-01'
159      }
160    ]
161    osDisk: {
162      caching: 'ReadWrite'
163      diskSizeGB: 128
164      managedDisk: {
165        storageAccountType: 'Standard_LRS'
166      }
167    }
168    osType: 'Linux'
169    vmSize: 'Standard_B2s_v2'
170    zone: 0
171    // Non-required parameters
172    location: location
173    managedIdentities: {
174      systemAssigned: true
175    }
176  }
177}
178
179module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
180  name: 'storageAccountDeployment'
181  params: {
182    // Required parameters
183    name: '${uniqueString(resourceGroup().id)}sa'
184    // Non-required parameters
185    location: location
186    skuName: 'Standard_LRS'
187    diagnosticSettings: [
188      {
189        name: 'storageAccountDiagnostics'
190        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
191      }
192    ]
193    publicNetworkAccess: 'Disabled'
194    allowBlobPublicAccess: false
195    blobServices: {
196      containers: [
197        {
198          name: 'vmstorage'
199          publicAccess: 'None'
200        }
201      ]
202      roleAssignments:[
203        {
204          principalId: virtualMachine.outputs.systemAssignedMIPrincipalId
205          principalType: 'ServicePrincipal'
206          roleDefinitionName: 'Storage Blob Data Contributor' // Allows read/write/delete on blob containers
207        }
208      ]
209    }
210    privateEndpoints: [
211      {
212        service: 'Blob'
213        subnetResourceId: virtualNetwork.outputs.subnetResourceIds[1] // Private Endpoint Subnet
214        privateDnsZoneGroup: {
215          privateDnsZoneGroupConfigs: [
216            {
217              privateDnsZoneResourceId: privateDnsBlob.outputs.resourceId
218            }
219          ]
220        }
221      }
222    ]
223  }
224}
225
226module privateDnsBlob 'br/public:avm/res/network/private-dns-zone:0.7.1' = {
227  name: '${prefix}-privatedns-blob'
228  params: {
229    name: 'privatelink.blob.${environment().suffixes.storage}'
230    location: 'global'
231    virtualNetworkLinks: [
232      {
233        name: '${virtualNetwork.outputs.name}-vnetlink'
234        virtualNetworkResourceId: virtualNetwork.outputs.resourceId
235      }
236    ]
237  }
238}
239
240// Note: Deploying a Bastion Host will automatically create a Public IP and use the subnet named "AzureBastionSubnet"
241// within our VNet. This subnet is required and must be named exactly "AzureBastionSubnet" for the Bastion Host to work.
242module bastion 'br/public:avm/res/network/bastion-host:0.6.1' = {
243  name: 'bastionDeployment'
244  params: {
245    name: '${prefix}-bastion'
246    virtualNetworkResourceId: virtualNetwork.outputs.resourceId
247    skuName: 'Basic'
248    location: location
249    diagnosticSettings: [
250      {
251        name: 'bastionDiagnostics'
252        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
253      }
254    ]
255  }
256}
Info

The Azure Subscription owner will have CRUD permissions for the Storage Account but not for the Key Vault. The Key Vault requires explicit RBAC permissions assigned to a user to grant them access: Provide access to Key Vaults using RBAC. Important!: at this point, you will only be able to access the Storage Account from the Bastion Host. Remember, public internet access has been disabled!

The RBAC policies have been successfully applied using a System-assigned Managed Identity on the Virtual Machine. This identity has been granted permissions on both the Key Vault and Storage Account. Now the VM can read secrets from the Key Vault and Read, Create, or Delete blobs in the Storage Account.

In a real production environment, the principle of least privileged access should be applied, providing only the exact permissions each service needs to carry out its functions. Learn more about Microsoft’s recommendations for identity and access management.

Conclusion

In this tutorial, we’ve explored how to leverage Azure Verified Modules (AVM) to build a secure, well-architected solution in Azure. AVM modules significantly simplify the deployment of Azure resources by abstracting away much of the complexity involved in configuring individual resources.

Your final, deployable Bicep template file should now look like this:

βž• Expand Code
  1param location string = 'westus2'
  2
  3@description('Required. A password for the VM admin user.')
  4@secure()
  5param vmAdminPass string
  6
  7var addressPrefix = '10.0.0.0/16'
  8var prefix = 'VM-AVM-Ex1'
  9
 10module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0.11.1' = {
 11  name: 'logAnalyticsWorkspace'
 12  params: {
 13    // Required parameters
 14    name: '${prefix}-law'
 15    // Non-required parameters
 16    location: location
 17  }
 18}
 19
 20module natGwPublicIp 'br/public:avm/res/network/public-ip-address:0.8.0' = {
 21  name: 'natGwPublicIpDeployment'
 22  params: {
 23    // Required parameters
 24    name: '${prefix}-natgwpip'
 25    // Non-required parameters
 26    location: location
 27    diagnosticSettings: [
 28      {
 29        name: 'natGwPublicIpDiagnostics'
 30        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 31      }
 32    ]
 33  }
 34}
 35
 36module natGateway 'br/public:avm/res/network/nat-gateway:1.2.2' = {
 37  name: 'natGatewayDeployment'
 38  params: {
 39    // Required parameters
 40    name: '${prefix}-natgw'
 41    zone: 1
 42    // Non-required parameters
 43    publicIpResourceIds: [
 44      natGwPublicIp.outputs.resourceId
 45    ]
 46  }
 47}
 48
 49module virtualNetwork 'br/public:avm/res/network/virtual-network:0.6.1' = {
 50  name: 'virtualNetworkDeployment'
 51  params: {
 52    // Required parameters
 53    addressPrefixes: [
 54      addressPrefix
 55    ]
 56    name: '${prefix}-vnet'
 57    // Non-required parameters
 58    location: location
 59    diagnosticSettings: [
 60      {
 61        name: 'vNetDiagnostics'
 62        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
 63      }
 64    ]
 65    subnets: [
 66      {
 67        name: 'VMSubnet'
 68        addressPrefix: cidrSubnet(addressPrefix, 24, 0) // first subnet in address space
 69        natGatewayResourceId: natGateway.outputs.resourceId
 70        networkSecurityGroupResourceId: nsgVM.outputs.resourceId
 71      }
 72      {
 73        name: 'PrivateEndpointSubnet'
 74        addressPrefix: cidrSubnet(addressPrefix, 24, 1) // second subnet in address space
 75      }
 76      {
 77        name: 'AzureBastionSubnet' // Azure Bastion Host requires this subnet to be named exactly "AzureBastionSubnet"
 78        addressPrefix: cidrSubnet(addressPrefix, 24, 2) // third subnet in address space
 79      }
 80    ]
 81  }
 82}
 83
 84module nsgVM 'br/public:avm/res/network/network-security-group:0.5.1' = {
 85  name: 'nsgVmDeployment'
 86  params: {
 87    name: '${prefix}-NSG-VM'
 88    location: location
 89    securityRules: [
 90      {
 91        name: 'AllowBastionSSH'
 92        properties: {
 93          access: 'Allow'
 94          direction: 'Inbound'
 95          priority: 100
 96          protocol: 'Tcp'
 97          sourceAddressPrefix: 'virtualNetwork'
 98          sourcePortRange: '*'
 99          destinationAddressPrefix: '*'
100          destinationPortRange: '22'
101        }
102      }
103    ]
104  }
105}
106
107module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = {
108  name: 'keyVaultDeployment'
109  params: {
110    // Required parameters
111    name: '${uniqueString(resourceGroup().id)}-kv'
112    // Non-required parameters
113    location: location
114    diagnosticSettings: [
115      {
116        name: 'keyVaultDiagnostics'
117        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
118      }
119    ]
120    enablePurgeProtection: false // disable purge protection for this example so we can more easily delete it
121    secrets: [
122      {
123        name: 'vmAdminPassword'
124        value: vmAdminPass
125      }
126    ]
127    roleAssignments: [
128      {
129        principalId: virtualMachine.outputs.systemAssignedMIPrincipalId
130        principalType: 'ServicePrincipal'
131        roleDefinitionIdOrName: 'Key Vault Secrets User' // Allows read access to secrets
132      }
133    ]
134  }
135}
136
137module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.14.0' = {
138  name: 'linuxVirtualMachineDeployment'
139  params: {
140    // Required parameters
141    adminUsername: 'localAdminUser'
142    adminPassword: vmAdminPass
143    imageReference: {
144      offer: '0001-com-ubuntu-server-jammy'
145      publisher: 'Canonical'
146      sku: '22_04-lts-gen2'
147      version: 'latest'
148    }
149    name: '${prefix}-vm1'
150    nicConfigurations: [
151      {
152        ipConfigurations: [
153          {
154            name: 'ipconfig01'
155            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0] // VMSubnet
156          }
157        ]
158        nicSuffix: '-nic-01'
159      }
160    ]
161    osDisk: {
162      caching: 'ReadWrite'
163      diskSizeGB: 128
164      managedDisk: {
165        storageAccountType: 'Standard_LRS'
166      }
167    }
168    osType: 'Linux'
169    vmSize: 'Standard_B2s_v2'
170    zone: 0
171    // Non-required parameters
172    location: location
173    managedIdentities: {
174      systemAssigned: true
175    }
176  }
177}
178
179module storageAccount 'br/public:avm/res/storage/storage-account:0.19.0' = {
180  name: 'storageAccountDeployment'
181  params: {
182    // Required parameters
183    name: '${uniqueString(resourceGroup().id)}sa'
184    // Non-required parameters
185    location: location
186    skuName: 'Standard_LRS'
187    diagnosticSettings: [
188      {
189        name: 'storageAccountDiagnostics'
190        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
191      }
192    ]
193    publicNetworkAccess: 'Disabled'
194    allowBlobPublicAccess: false
195    blobServices: {
196      containers: [
197        {
198          name: 'vmstorage'
199          publicAccess: 'None'
200        }
201      ]
202      roleAssignments:[
203        {
204          principalId: virtualMachine.outputs.systemAssignedMIPrincipalId
205          principalType: 'ServicePrincipal'
206          roleDefinitionName: 'Storage Blob Data Contributor' // Allows read/write/delete on blob containers
207        }
208      ]
209    }
210    privateEndpoints: [
211      {
212        service: 'Blob'
213        subnetResourceId: virtualNetwork.outputs.subnetResourceIds[1] // Private Endpoint Subnet
214        privateDnsZoneGroup: {
215          privateDnsZoneGroupConfigs: [
216            {
217              privateDnsZoneResourceId: privateDnsBlob.outputs.resourceId
218            }
219          ]
220        }
221      }
222    ]
223  }
224}
225
226module privateDnsBlob 'br/public:avm/res/network/private-dns-zone:0.7.1' = {
227  name: '${prefix}-privatedns-blob'
228  params: {
229    name: 'privatelink.blob.${environment().suffixes.storage}'
230    location: 'global'
231    virtualNetworkLinks: [
232      {
233        name: '${virtualNetwork.outputs.name}-vnetlink'
234        virtualNetworkResourceId: virtualNetwork.outputs.resourceId
235      }
236    ]
237  }
238}
239
240// Note: Deploying a Bastion Host will automatically create a Public IP and use the subnet named "AzureBastionSubnet"
241// within our VNet. This subnet is required and must be named exactly "AzureBastionSubnet" for the Bastion Host to work.
242module bastion 'br/public:avm/res/network/bastion-host:0.6.1' = {
243  name: 'bastionDeployment'
244  params: {
245    name: '${prefix}-bastion'
246    virtualNetworkResourceId: virtualNetwork.outputs.resourceId
247    skuName: 'Basic'
248    location: location
249    diagnosticSettings: [
250      {
251        name: 'bastionDiagnostics'
252        workspaceResourceId: logAnalyticsWorkspace.outputs.resourceId
253      }
254    ]
255  }
256}

AVM modules provide several key advantages over writing raw Bicep templates:

  1. Simplified Resource Configuration: AVM modules handle much of the complex configuration work behind the scenes
  2. Built-in Recommended Practices: The modules implement many of Microsoft’s recommended practices by default
  3. Consistent Outputs: Each module exposes a consistent set of outputs that can be easily referenced
  4. Reduced Boilerplate Code: What would normally require hundreds of lines of Bicep code can be accomplished in a fraction of the space

As you continue your journey with Azure and AVM, remember that this approach can be applied to more complex architectures as well. The modular nature of AVM allows you to mix and match components to build solutions that meet your specific needs while adhering to Microsoft’s Well-Architected Framework.

By using AVM modules as building blocks, you can focus more on your solution architecture and less on the intricacies of individual resource configurations, ultimately leading to faster development cycles and more reliable deployments.

Clean up your environment

When you are ready, you can remove the infrastructure deployed in this example. Key Vaults are set to a soft-delete state so you will also need to purge the one we created in order to fully delete it. The following commands will remove all resources created by your deployment:

Clean up with
# Delete the resource group
Remove-AzResourceGroup -Name "avm-bicep-vmexample1" -Force

# Purge the Key Vault
Remove-AzKeyVault -VaultName "<keyVaultName>" -Location "<location>" -InRemovedState -Force
# Delete the resource group
az group delete --name 'avm-bicep-vmexample1' --yes --no-wait

# Purge the Key Vault
az keyvault purge --name '<keyVaultName>' --no-wait

Congratulations, you have successfully leveraged AVM Bicep modules to deploy resources in Azure!

Tip

We welcome your contributions and feedback to help us improve the AVM modules and the overall experience for the community!

Terraform - Solution Development

Introduction

Azure Verified Modules (AVM) for Terraform are a powerful tool that leverage the Terraform domain-specific language (DSL), industry knowledge, and an Open Source community, which altogether enable developers to quickly deploy Azure resources that follow Microsoft’s recommended practices for Azure.
In this article, we will walk through the Terraform specific considerations and recommended practices on developing your solution leveraging Azure Verified Modules. We’ll review some of the design features and trade-offs and include sample code to illustrate each discussion point.

Prerequisites

You will need the following tools and components to complete this guide:

Before you begin, ensure you have these tools installed in your development environment.

Planning

Good module development should start with a good plan. Let’s first review the architecture and module design prior to developing our solution.

Solution Architecture

Before we begin coding, it is important to have details about what the infrastructure architecture will include. For our example, we will be building a solution that will host a simple application on a Linux virtual machine (VM).

In our design, the resource group for our solution will require appropriate tagging to comply with our corporate standards. Resources that support Diagnostic Settings must also send metric data to a Log Analytics workspace, so that the infrastructure support teams can get metric telemetry. The virtual machine will require outbound internet access to allow the application to properly function. A Key Vault will be included to store any secrets and key artifacts, and we will include a Bastion instance to allow support personnel to access the virtual machine if needed. Finally, the VM is intended to run without interaction, so we will auto-generate an SSH private key and store it in the Key Vault for the rare event of someone needing to log into the VM.

Based on this narrative, we will create the following resources:

  • A resource group to contain all the resources with tagging
  • A random string resource for use in resources with global naming (Key Vault)
  • A Log Analytics workspace for diagnostic data
  • A Key Vault with:
    • Role-Based Access Control (RBAC) to allow data access
    • Logging to the Log Analytics workspace
  • A virtual network with:
    • A virtual machine subnet
    • A Bastion subnet
    • Network Security Group on the VM subnet allowing SSH traffic
    • Logging to the Log Analytics workspace
  • A NAT Gateway for enabling outbound internet access
    • Associated to the virtual machine subnet
  • A Bastion service for secure remote access to the Virtual Machine
    • Logging to the Log Analytics workspace
  • A virtual machine resource with
    • A single private IPv4 interface attached to the VM subnet
    • A randomly generated admin account private key stored in the Key Vault
    • Metrics sent to the log Analytics workspace
Azure VM Solution Architecture

Solution template (root module) design

Since our solution template (root module) is intended to be deployed multiple times, we want to develop it in a way that provides flexibility while minimizing the amount of input necessary to deploy the solution. For these reasons, we will create our module with a small set of variables that allow for deployment differentiation while still populating solution-specific defaults to minimize input. We will also separate our content into variables.tf, outputs.tf, terraform.tf, and main.tf files to simplify future maintenance.

Based on this, our file system will take the following structure:

  • Module Directory
    • terraform.tf - This file holds the provider definitions and versions.
    • variables.tf - This file contains the input variable definitions and defaults.
    • outputs.tf - This file contains the outputs and their descriptions for use by any external modules calling this root module.
    • main.tf - This file contains the core module code for creating the solutions infrastructure.
    • development.tfvars - This file will contain the inputs for the instance of the module that is being deployed. Content in this file will vary from instance to instance.
Note

Terraform will merge content from any file ending in a .tf extension in the module folder to create the full module content. Because of this, using different files is not required. We encourage file separation to allow for organizing code in a way that makes it easier to maintain. While the naming structure we’ve used is common, there are many other valid file naming and organization options that can be used.

In our example, we will use the following variables as inputs to allow for customization:

  • location - The location where our infrastructure will be deployed.
  • name_prefix - This will be used to preface all of the resource naming.
  • virtual_network_prefix - This will be used to ensure IP uniqueness for the deployment.
  • tags - The custom tags to use for each deployment.

Finally, we will export the following outputs:

  • resource_group_name - This will allow for finding this deployment if there are multiples.
  • virtual_machine_name - This can be used to find and login to the vm if needed.

Identifying AVM modules that match our solution

Now that we’ve determined our architecture and module configurations, we need to see what AVM modules exist for use in our solution. To do this, we will open the AVM Terraform pattern module index and check if there are any existing pattern modules that match our requirement. In this case, no pattern modules fit our needs. If this was a common pattern, we could open an issue on the AVM github repository to get assistance from the AVM project to create a pattern module matching our requirements. Since our architecture isn’t common, we’ll continue to the next step.

When a pattern module fitting our needs doesn’t exist for a solution, leveraging AVM resource modules to build our own solution is the next best option. Review the AVM Terraform published resource module index for each of the resource types included in your architecture. For each AVM module, capture a link to the module to allow for a review of the documentation details on the Terraform Registry website.

Note

Some of the published pattern modules cover multi-resource configurations that can sometimes be interpreted as a single resource. Be sure to check the pattern index for groups of resources that may be part of your architecture and that don’t exist in the resource module index. (e.g., Virtual WAN)

For our sample architecture, we have the following AVM resource modules at our disposal. Click on each module to explore its documentation on the Terraform Registry.

Develop the Solution Code

We can now begin coding our solution. We will create each element individually, to allow us to test our deployment as we build it out. This will also allow us to correct any bugs incrementally, so that we aren’t troubleshooting a large number of resources at the end.

Creating the terraform.tf file

Let’s begin by configuring the provider details necessary to build our solution. Since this is a root module, we want to include any provider and Terraform version constraints for this module. We’ll periodically come back and add any needed additional providers if our design includes a resource from a new provider.

Open up your development IDE (Visual studio code in our example) and create a file named terraform.tf in your root directory.

Add the following code to your terraform.tf file:

βž• Expand Code
1terraform {
2  required_version = "~> 1.9"
3  required_providers {
4  }
5}
Note

Always click on the “Copy to clipboard” button in the top right corner of the Code sample area in order not to have the line numbers included in the copied code.

This specifies that the required Terraform binary version to run your module can be any version between 1.9 and 2.0. This is a good compromise for allowing a range of binary versions while also ensuring support for any required features that are used as part of the module. This can include things like newly introduced functions or support for new key words.

Since we are developing our solution incrementally, we should validate our code. To do this, we will take the following steps:

  1. Open up a terminal window if it is not already open. In some IDE’s this can be done as a function of the IDE.
  2. Change directory to the module directory by typing cd and then the path to the module. As an example, if the module directory was named example we would run cd example.
  3. Run terraform init to initialize your provider file.

You should now see a message indicating that Terraform has been successfully initialized. This indicates that our code is error free and we can continue on. If you get errors, examine the provider syntax for typos, missing quotes, or missing brackets.

Creating a variables.tf file

Because our module is intended to be reusable, we want to provide the capability to customize each module call with those items that will differ between them. This is done by using variables to accept inputs into the module. We’ll define these inputs in a separate file named variables.tf.

Go back to the IDE, and create a file named variables.tf in the working directory.

Add the following code to your variables.tf file to configure the inputs for our example:

βž• Expand Code
 1variable "name_prefix" {
 2  description = "Prefix for the name of the resources"
 3  type        = string
 4  default     = "example"
 5}
 6
 7variable "location" {
 8  description = "The Azure location to deploy the resources"
 9  type        = string
10  default     = "East US"
11}
12
13variable "virtual_network_cidr" {
14  description = "The CIDR prefix for the virtual network. This should be at least a /22. Example 10.0.0.0/22"
15  type        = string
16}
17
18variable "tags" {
19  description = "Tags to be applied to all resources"
20  type        = map(string)
21  default     = {}
22}
Note

Note that each variable definition includes a type definition to guide module users on how to properly define an input. Also note that it is possible to set a default value. This allows module consumers to avoid setting a value if they find the default to be acceptable.

We should now test the new content we’ve created for our module. To do this, first re-run terraform init on your command line. Note that nothing has changed and the initialization completes successfully. Since we now have module content, we will attempt to run the plan as the next step of the workflow.

Type terraform plan on your command line. Note that it now asks for us to provide a value for the var.virtual_network_cidr variable. This is because we don’t provide a default value for that input so Terraform must have a valid input before it can continue. Type 10.0.0.0/22 into the input and press enter to allow the plan to complete. You should now see a message indicating that Your infrastructure matches the configuration and that no changes are needed.

Creating a development.tfvars file

There are multiple ways to provide input to the module we’re creating. We will create a tfvars file that can be supplied during plan and apply stages to minimize the need for manual input. tfvars files are a nice way to document inputs as well as allow for deploying different versions of your module. This is useful if you have a pipeline where infrastructure code is deployed first for development, and then is deployed for QA, staging, or production with different input values.

In your IDE, create a new file named development.tfvars in your working directory.

Now add the following content to your development.tfvars file.

βž• Expand Code
1location = "westus2"
2prefix = dev
3virtual_network_cidr = "10.1.0.0/22"
4tags = {
5  environment = "development"
6  owner       = "dev-team"
7}
Note

Note that each variable has a value defined. Although, only inputs without default values are required, we include values for all of the inputs for clarity. Consider doing this in your environments so that someone looking at the tfvars files has a full picture of what values are being set.

Re-run the terraform apply, but this time, reference the .tfvars file by using the following command: terraform plan -var-file=development.tfvars. You should get a successful completion without needing to manually provide inputs.

Creating the main.tf file

Now that we’ve created the supporting files, we can start building the actual infrastructure code in our main file. We will add one AVM resource module at a time so that we can test each as we implement them.

Return to your IDE and create a new file named main.tf.

Add a resource group

In Azure, we need a resource group to hold any infrastructure resources we create. This is a simple resource that typically wouldn’t require an AVM module, but we’ll include the AVM module so we can take advantage of the Role-Based Access Control (RBAC) interface if we need to restrict access to the resource group in future versions.

First, let’s visit the Terraform registry documentation page for the resource group and explore several key sections.

  1. Note the Provision Instructions box on the right-hand side of the page. This contains the module source and version details which allows us to copy the latest version syntax without needing to type everything ourselves.
  2. Now review the Readme tab in the middle of the page. It contains details about all required and optional inputs, resources that are created with the module, and any outputs that are defined. If you want to explore any of these items in detail, each element has a tab that you can review as needed.
  3. Finally, in the middle of the page, there is a drop-down menu named Examples that contains functioning examples for the AVM module. These showcase a good example of using copy/paste to bootstrap module code and then modify it for your specific purpose.

Now that we’ve explored the registry content, let’s add a resource group to our module.

First, copy the content from the Provision Instructions box into our main.tf file.

βž• Expand Code
1module "avm-res-resources-resourcegroup" {
2  source  = "Azure/avm-res-resources-resourcegroup/azurerm"
3  version = "0.2.1"
4  # insert the 2 required variables here
5}

On the modules documentation page, go to the inputs tab. Review the Required Inputs tab. These are the values that don’t have defaults and are the minimum required values to deploy the module. There are additional inputs in the Optional Inputs section that can be used to configure additional module functionality. Review these inputs and determine which values you would like to define in your AVM module call.

Now, replace the # insert the 2 required variables here comment with the following code to define the module inputs. Our main.tf code should look like the following:

βž• Expand Code
1module "avm-res-resources-resourcegroup" {
2  source  = "Azure/avm-res-resources-resourcegroup/azurerm"
3  version = "0.2.1"
4
5  name = "${var.name_prefix}-rg"
6  location = var.location
7  tags = var.tags
8}
Note

Note how we’ve used the prefix variable and Terraform interpolation syntax to dynamically name the resource group. This allows for module customization and re-use. Also note that even though we chose to use the default module name of avm-res-resources-resourcegroup, we could modify the name of the module if needed.

After saving the file, we want to test our new content. To do this, return to the command line and first run terraform init. Notice how Terraform has downloaded the module code, as well as providers that the module requires. In this case, you can see the azurerm, random, and modtm providers were downloaded.

Let’s now deploy our resource group. First, let’s run a plan operation to review what will be created. Type terraform plan -var-file=development.tfvars and press enter to initiate the plan.

Add the features block

Notice that we get an error indicating that we are Missing required argument and that for the azurerm provider, we need to provide a features argument. The addition of the resource group AVM resource requires that the azurerm provider be installed to provision resources in our module. This provider requires a features block in its provider definition that is missing in our configuration.

Return to the terraform.tf file and add the following content to it. Note how the features block is currently empty. If we needed to activate any feature flags in our module, we could add them here.

βž• Expand Code
 1terraform {
 2  required_version = "~> 1.9"
 3  required_providers {
 4  }
 5}
 6
 7provider "azurerm" {
 8  features {
 9  }
10}

Re-run terraform plan -var-file=development.tfvars now that we have updated the features block.

Set the subscription ID

Note that we once again get an error. This time, the error indicates that subscription_id is a required provider property for plan/apply operations. This is a change that was introduced as part of the version 4 release of the AzureRM provider. We need to supply the ID of the deployment subscription where our resources will be created.

First, we need to get the subscription ID value. We will use the portal for this exercise, but using the Azure CLI, PowerShell, or the resource graph will also work to retrieve this value.

  1. Open the Azure portal.
  2. Enter Subscriptions in the search field at the top middle of the page.
  3. Select Subscriptions from the services menu in the search drop-down.
  4. Select the subscription you wish to deploy to, from the list of subscriptions.
  5. Find the Subscription ID field on the overview page and click the copy button to copy it to the clipboard.

Secondly, we need to update Terraform so that it can use the subscription ID. There are multiple ways to provide a subscription ID to the provider including adding it to the features block or using environment variables. For this scenario we’ll use environment variables to set the values so that we don’t have to re-enter them on each run. This also keeps us from storing the subscription ID in our code since it is considered a sensitive value. Select a command from the list below based on your operating system.

  1. (Linux/MacOS) - Run the following command with your subscription ID: export ARM_SUBSCRIPTION_ID=<your ID here>
  2. (Windows) - Run the following command with your subscription ID: set ARM_SUBSCRIPTION_ID=<your ID here>

Finally, we should now be able to complete our plan operation by re-running terraform plan -var-file=development.tfvars. Note that the plan will create three resources, two for telemetry and one for the resource group.

Deploy the resource group

We can complete testing by implementing the resource group. Run terraform apply -var-file="development.tfvars" and type yes and press enter when prompted to accept the changes. Terraform will create the resource group and notify you with a Apply complete message and a summary of the resources that were added, changed, and destroyed.

Deploy the Log Analytics Workspace

We can now continue by adding the Log Analytics Workspace to our main.tf file. We will follow a workflow similar to what we did with the resource group.

  1. Browse to the AVM Log Analytics Workspace module page in the Terraform Registry.
  2. Copy the module content from the Provision Instructions portion of the page into the main.tf file.

This time, instead of manually supplying module inputs, we will copy module content from one of the examples to minimize the amount of typing required. In most examples, the AVM module call is located at the bottom of the example.

  1. Navigate to the Examples drop-down menu in the documentation and select the default example from the menu. You will see a fully functioning example code which includes the module and any supporting resources. Since we only care about the workspace resource from this example, we can scroll to the bottom of the code block and find the module "log_analytics_workspace" line.
  2. Copy the content between the module brackets with the exception of the line defining the module source. Because these examples are part of the testing methodology for the module, they use a dot reference value (../..) for the module source value which will not work in our module call. To work around this, we copied those values from the provision instructions section of the module documentation in a previous step.
  3. Update the location and resource group name values to reference outputs from the resource group module. Using implicit references such as these allow Terraform to determine the order in which resources should be built.
  4. Update the name field using the prefix variable to allow for customization using a similar pattern to what we used on the resource group.

The Log Analytics module content should look like the following code block. For simplicity, you can also copy this directly to avoid multiple copy/paste actions.

βž• Expand Code
 1module "avm-res-operationalinsights-workspace" {
 2  source  = "Azure/avm-res-operationalinsights-workspace/azurerm"
 3  version = "0.4.2"
 4
 5  enable_telemetry                          = true
 6  location                                  = module.avm-res-resources-resourcegroup.resource.location
 7  resource_group_name                       = module.avm-res-resources-resourcegroup.name
 8  name                                      = "${var.name_prefix}-law"
 9  log_analytics_workspace_retention_in_days = 30
10  log_analytics_workspace_sku               = "PerGB2018"
11}

Again, we will need to run terraform init to allow Terraform to initialize a copy of the AVM Log Analytics module.

Now, we can deploy the Log Analytics workspace by running terraform apply -var-file="development.tfvars", typing yes and pressing enter. Note that Terraform will only create the new Log Analytics resources since the resource group already exists. This is one of the key benefits of deploying using Infrastructure as Code (IAC) tools like Terraform.

Note

Note that we ran the terraform apply command without first running terraform plan. Because terraform apply runs a plan before prompting for the apply, we opted to shorten the instructions by skipping the explicit plan step. If you are testing in a live environment, you may want to run the plan step and save the plan as part of your governance or change control processes.

Deploy the Azure Key Vault

Our solution calls for a simple Key Vault implementation to store virtual machine secrets. We’ll follow the same workflow for deploying the Key Vault as we used for the previous resource group and Log Analytics workspace resources. However, since Key Vaults require data roles to manage secrets and keys, we will need to use the RBAC interface and a data resource to configure Role-Based Access Control (RBAC) during the deployment.

Note

For this exercise, we will provision the deployment user with data rights on the Key Vault. In your environment, you will likely want to either provide additional roles as inputs or statically assign users, or groups to the Key Vault data roles. For simplicity we also set the Key Vault to have public access enabled due to us not being able to dictate a private deployment environment. In your environment where your deployment machine will be on a private network it is recommended to restrict public access for the Key Vault.

Before we implement the AVM module for the Key Vault, we want to use a data resource to read the client details about the user context of the current Terraform deployment.

Add the following line to your main.tf file and save it.

βž• Expand Code
1data "azurerm_client_config" "this" {}

Key vaults use a global namespace which means that we will also need to add a randomization resource to allow us to randomize the name to avoid any potential name intersection issues with other Key Vault deployments. We will use Terraform’s random provider to generate the random string which we will append to the Key Vault name. Add the following code to your main module to create the random_string resource we will use for naming.

βž• Expand Code
1resource "random_string" "name_suffix" {
2  length  = 4
3  special = false
4  upper   = false
5}

Now we can continue with adding the AVM Key Vault module to our solution.

  1. Browse to the AVM Key Vault resource module page in the Terraform Registry.
  2. Copy the module content from the Provision Instructions portion of the page into the main.tf file.
  3. This time, we’re going to select relevant content from the Create secret example to fill out our module.
  4. Copy the name, location, enable_telemetry, resource_group_name, tenant_id, and role_assignments value content from the example and paste it into the new Key Vault module in your solution.
  5. Update the name value to be "${var.prefix}-kv-${random_string.name_suffix.result}"
  6. Update the location and resource_group_name values to the same implicit resource group module references we used in the Log Analytics workspace.
  7. Set the enable_telemetry value to true.
  8. Leave the tenant_id and role_assignments values to the same values that are in the example.

Our architecture calls for us to include a diagnostic settings configuration for each resource that supports it. We’ll use the diagnostic-settings example to copy this content.

  1. Return to the documentation page and select the diagnostic-settings option from the examples drop-down.
  2. Locate the Key Vault resource in the example’s code block and copy the diagnostic_settings value and paste it into the Key Vault module block we’re building in main.tf.
  3. Update the name value to use our prefix variable to allow for name customization.
  4. Update the workspace_resource_id value to be an implicit reference to the output from the previously implemented Log Analytics module (module.avm-res-operationalinsights-workspace.resource_id in our code).

Finally, we will allow public access, so that our deployer machine can add secrets to the Key Vault. If your environment doesn’t allow public access for Key Vault deployments, locate the public IP address of your deployer machine (this may be an external NAT IP for your network) and add it to the network_acls.ip_rules list value using CIDR notation.

  1. Set the network_acls input to null in your module block for the Key Vault.

Your Key Vault module definition should now look like the following:

βž• Expand Code
 1module "avm-res-keyvault-vault" {
 2  source  = "Azure/avm-res-keyvault-vault/azurerm"
 3  version = "0.10.0"
 4
 5  enable_telemetry    = true
 6  location            = module.avm-res-resources-resourcegroup.resource.location
 7  resource_group_name = module.avm-res-resources-resourcegroup.name
 8  name                = "${var.name_prefix}-kv-${random_string.name_suffix.result}"
 9  tenant_id           = data.azurerm_client_config.this.tenant_id
10  network_acls        = null
11
12  diagnostic_settings = {
13    to_la = {
14      name                  = "${var.name_prefix}-kv-diags"
15      workspace_resource_id = module.avm-res-operationalinsights-workspace.resource_id
16    }
17  }
18
19  role_assignments = {
20    deployment_user_kv_admin = {
21      role_definition_id_or_name = "Key Vault Administrator"
22      principal_id               = data.azurerm_client_config.this.object_id
23    }
24  }
25}
Note

One of the core values of AVM is the standard configuration for interfaces across modules. The Role Assignments interface we used as part of the Key Vault deployment is a good example of this.

Continue the incremental testing of your module by running another terraform init and terraform apply -var-file="development.tfvars" sequence.

Deploy the NAT Gateway

Our architecture calls for a NAT Gateway to allow virtual machines to access the internet. We will use the NAT Gateway resource_id output in future modules to link the virtual machine subnet.

  1. Browse to the AVM NAT Gateway resource module page in the Terraform Registry.
  2. Copy the module definition and source from the Provision Instructions card from the module main page.
  3. Copy the remaining module content from the default example excluding the subnet associations map, as we will do the association when we build the vnet.
  4. Update the location and resource_group_nameusing implicit references from our resource group module.
  5. Then update each of the name values to use the name_prefix variables.

Review the following code to see each of these changes.

βž• Expand Code
 1module "avm-res-network-natgateway" {
 2  source  = "Azure/avm-res-network-natgateway/azurerm"
 3  version = "0.2.1"
 4
 5  name                = "${var.name_prefix}-natgw"
 6  enable_telemetry    = true
 7  location            = module.avm-res-resources-resourcegroup.resource.location
 8  resource_group_name = module.avm-res-resources-resourcegroup.name
 9
10  public_ips = {
11    public_ip_1 = {
12      name = "${var.name_prefix}-natgw-pip"
13    }
14  }
15}

Continue the incremental testing of your module by running another terraform init and terraform apply -var-file="development.tfvars" sequence.

Deploy the Network Security Group

Our architecture calls for a Network Security Group (NSG) allowing SSH access to the virtual machine subnet. We will use the NSG AVM resource module to accomplish this task.

  1. Browse to the AVM Network Security Group resource module page in the Terraform Registry.
  2. Copy the module definition and source from the Provision Instructions card from the module main page.
  3. Copy the remaining module content from the example_with_NSG_rule example.
  4. Update the location and resource_group_nameusing implicit references from our resource group module.
  5. Update the name value using the name_prefix variable interpolation as we did with the other modules.
  6. Copy the map entry labeled rule02 from the locals nsg_rules map and paste it between two curly braces to create the security_rules attribute in the NSG module we’re building.
  7. Make the following updates to the rule details:
    1. Rename the map key to "rule01" from "rule02".
    2. Update the name to use the var.prefix interpolation and SSH to describe the rule.
    3. Update the destination_port_ranges list to be ["22"].

Upon completion the code for the NSG module should be as follows:

βž• Expand Code
 1module "avm-res-network-networksecuritygroup" {
 2  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
 3  version = "0.4.0"
 4  resource_group_name = module.avm-res-resources-resourcegroup.name
 5  name                = "${var.name_prefix}-vm-subnet-nsg"
 6  location            = module.avm-res-resources-resourcegroup.resource.location
 7
 8  security_rules = {
 9    "rule01" = {
10      name                       = "${var.name_prefix}-ssh"
11      access                     = "Allow"
12      destination_address_prefix = "*"
13      destination_port_ranges    = ["22"]
14      direction                  = "Inbound"
15      priority                   = 200
16      protocol                   = "Tcp"
17      source_address_prefix      = "*"
18      source_port_range          = "*"
19    }
20  }
21}

Continue the incremental testing of your module by running another terraform init and terraform apply -var-file="development.tfvars" sequence.

Deploy the Virtual Network

We can now continue the build-out of our architecture by configuring the virtual network (vnet) deployment. This will follow a similar pattern as the previous resource modules, but this time, we will also add some network functions to help us customize the subnet configurations.

  1. Browse to the AVM Virtual Network resource module page in the Terraform Registry.
  2. Copy the module definition and source from the Provision Instructions card from the module main page.
  3. After looking through the examples, this time, we’ll use the complete example as a source to copy our content.
  4. Copy the resource_group_name, location, name, and address_space lines and replace their values with our deployment specific variables or module references.
  5. We’ll copy the subnets map and duplicate the subnet0 map for each subnet.
  6. Now we will update the map key and name values for each subnet so that they are unique.
  7. Then we’ll use the cidrsubnet function to dynamically generate the CIDR range for each subnet. You can explore the function documentation for more details on how it can be used.
  8. We will also populate the nat_gateway object on subnet0 with the resource_id output from our NAT Gateway module.
  9. To configure the NSG on the VM subnet we need to link it. Add a network_security_group attribute to the subnet0 definition and replace the value with the resource_id output from the NSG module.
  10. Finally, we’ll copy the diagnostic settings from the example and update the implicit references to point to our previously deployed Log Analytics workspace.

After making these changes our virtual network module call code will be as follows:

βž• Expand Code
 1module "avm-res-network-virtualnetwork" {
 2  source  = "Azure/avm-res-network-virtualnetwork/azurerm"
 3  version = "0.8.1"
 4
 5  resource_group_name = module.avm-res-resources-resourcegroup.name
 6  location            = module.avm-res-resources-resourcegroup.resource.location
 7  name                = "${var.name_prefix}-vnet"
 8
 9  address_space = [var.virtual_network_cidr]
10
11  subnets = {
12    subnet0 = {
13      name                            = "${var.name_prefix}-vm-subnet"
14      default_outbound_access_enabled = false
15      address_prefixes = [cidrsubnet(var.virtual_network_cidr, 1, 0)]
16      nat_gateway = {
17        id = module.avm-res-network-natgateway.resource_id
18      }
19      network_security_group = {
20        id = module.avm-res-network-networksecuritygroup.resource_id
21      }
22    }
23    bastion = {
24      name                            = "AzureBastionSubnet"
25      default_outbound_access_enabled = false
26      address_prefixes = [cidrsubnet(var.virtual_network_cidr, 1, 1)]
27    }
28  }
29
30  diagnostic_settings = {
31    sendToLogAnalytics = {
32      name                           = "${var.name_prefix}-vnet-diagnostic"
33      workspace_resource_id          = module.avm-res-operationalinsights-workspace.resource_id
34      log_analytics_destination_type = "Dedicated"
35    }
36  }
37}
Note

Note how the Log Analytics workspace reference ends in resource_id. Each AVM module is required to export its Azure resource ID with the resource_id name to allow for consistent references.

Continue the incremental testing of your module by running another terraform init and terraform apply -var-file="development.tfvars" sequence.

Deploy the Bastion service

We want to allow for secure remote access to the virtual machine for configuration and troubleshooting tasks. We’ll use Azure Bastion to accomplish this objective following a similar workflow to our other resources.

  1. Browse to the AVM Bastion resource module page in the Terraform Registry.
  2. Copy the module definition and source from the Provision Instructions card from the module main page.
  3. Copy the remaining module content from the Simple Deployment example.
  4. Update the location and resource_group_nameusing implicit references from our resource group module.
  5. Update the name value using the name_prefix variable interpolation as we did with the other modules.
  6. Finally, update the subnet_id value to include an implicit reference to the bastion keyed subnet from our virtual network module.

Our architecture calls for diagnostic settings to be configured on the Azure Bastion resource. In this case, there aren’t any examples that include this configuration. However, since the diagnostic settings interface is one of the standard interfaces in Azure Verified Modules, we can just copy the interface definition from our virtual network module.

  1. Locate the virtual network module and copy the diagnostic_settings value from it.
  2. Paste the diagnostic_settings value into the code for our Bastion module.
  3. Update the diagnostic setting’s name value from vnet to Bastion.

The new code we added for the Bastion resource will be as follows:

βž• Expand Code
 1module "avm-res-network-bastionhost" {
 2  source  = "Azure/avm-res-network-bastionhost/azurerm"
 3  version = "0.7.2"
 4
 5  name                = "${var.name_prefix}-bastion"
 6  resource_group_name = module.avm-res-resources-resourcegroup.name
 7  location            = module.avm-res-resources-resourcegroup.resource.location
 8  ip_configuration = {
 9    subnet_id = module.avm-res-network-virtualnetwork.subnets["bastion"].resource_id
10  }
11
12  diagnostic_settings = {
13    sendToLogAnalytics = {
14      name                           = "${var.name_prefix}-bastion-diagnostic"
15      workspace_resource_id          = module.avm-res-operationalinsights-workspace.resource_id
16      log_analytics_destination_type = "Dedicated"
17    }
18  }
19}
Note

Pay attention to the subnet_id syntax. In the virtual network module, the subnets are created as a sub-module allowing us to reference each of them using the map key that was defined in the subnets input. Again, we see the consistent output naming with the resource_id output for the sub-module.

Continue the incremental testing of your module by running another terraform init and terraform apply -var-file="development.tfvars" sequence.

Deploy the virtual machine

The final step in our deployment will be our application virtual machine. We’ve had good success with our workflow so far, so we’ll use it for this step as well.

  1. Browse to the AVM Virtual Machine resource module page in the Terraform Registry.
  2. Copy the module definition and source from the Provision Instructions card from the module main page.
  3. Copy the remaining module content from the linux_default example.
  4. Update the location and resource_group_nameusing implicit references from our resource group module.
  5. To be compliant with Well Architected Framework guidance we encourage defining a zone if your region supports it. Update the zone input to 1.
  6. Update the sku_size input to “Standard_D2s_v5”.
  7. Update the name values using the name_prefix variable interpolation as we did with the other modules and include the output from the random_string.name_suffix resource to add uniqueness.
  8. Set the account_credentials.key_vault_configuration.resource_id value to reference the resource_id output from the Key Vault module.
  9. Update the private_ip_subnet_resource_id value to an implicit reference to the subnet0 subnet output from the virtual network module.

Because the default Linux example doesn’t include diagnostic settings, we need to add that content in a different way. Since the diagnostic settings interface has a standard schema, we can copy the diagnostic_settings input from our virtual network module.

  1. Locate the virtual network module in your code and copy the diagnostic_settings map from it.
  2. Paste the diagnostic_settings content into your virtual machine module code.
  3. Update the name value to reflect that it applies to the virtual machine.

The new code we added for the virtual machine resource will be as follows:

βž• Expand Code
 1module "avm-res-compute-virtualmachine" {
 2  source  = "Azure/avm-res-compute-virtualmachine/azurerm"
 3  version = "0.19.1"
 4
 5  enable_telemetry    = true
 6  location            = module.avm-res-resources-resourcegroup.resource.location
 7  resource_group_name = module.avm-res-resources-resourcegroup.name
 8  name                = "${var.name_prefix}-vm"
 9  os_type             = "Linux"
10  sku_size            = "Standard_D2s_v5"
11  zone                = 1
12
13  source_image_reference = {
14    publisher = "Canonical"
15    offer     = "0001-com-ubuntu-server-focal"
16    sku       = "20_04-lts-gen2"
17    version   = "latest"
18  }
19
20  network_interfaces = {
21    network_interface_1 = {
22      name = "${var.name_prefix}-nic-${random_string.name_suffix.result}"
23      ip_configurations = {
24        ip_configuration_1 = {
25          name                          = "${var.name_prefix}-ipconfig-${random_string.name_suffix.result}"
26          private_ip_subnet_resource_id = module.avm-res-network-virtualnetwork.subnets["subnet0"].resource_id
27        }
28      }
29    }
30  }
31
32  diagnostic_settings = {
33    sendToLogAnalytics = {
34      name                           = "${var.name_prefix}-vm-diagnostic"
35      workspace_resource_id          = module.avm-res-operationalinsights-workspace.resource_id
36      log_analytics_destination_type = "Dedicated"
37    }
38  }
39}

Continue the incremental testing of your module by running another terraform init and terraform apply -var-file="development.tfvars" sequence.

Creating the outputs.tf file

The final piece of our module is to export any values that may need to be consumed by module users. From our architecture, we’ll export the resource group name and the virtual machine resource name.

  1. Create an outputs.tf file in your IDE.
  2. Create an output named resource_group_name and set the value to an implicit reference to the resource group modules name output. Include a brief description for the output.
  3. Create an output named virtual_machine_name and set the value to an implicit reference to the virtual machine module’s name output. Include a brief description for the output.

The new code we added for the outputs will be as follows:

βž• Expand Code
1output "resource_group_name" {
2  value =  module.avm-res-resources-resourcegroup.name
3  description = "The resource group name where the resources are deployed"
4}
5
6output "virtual_machine_name" {
7    value = module.avm-res-compute-virtualmachine.name
8    description = "The name of the virtual machine"
9}

Because no new modules were created, we don’t need to run terraform init to test this change. Run terraform apply -var-file="development.tfvars" to see the new outputs that have been created.

Update the terraform.tf file

It is a recommended practice to define the required versions of the providers for your module to ensure consistent behavior when it is being run. In this case we are going to be slightly permissive and allow increases in minor and patch versions to fluctuate, since those are not supposed to include breaking changes. In a production environment, you would likely want to pin on a specific version to guarantee behavior.

  1. Run terraform init to review the providers and versions that are currently installed.
  2. Update your terraform.tf file’s required providers field for each provider listed in the downloaded providers.

The updated code we added for the providers in the terraform.tf file will be as follows:

βž• Expand Code
 1terraform {
 2  required_version = "~> 1.9"
 3  required_providers {
 4    azapi = {
 5      source  = "azure/azapi"
 6      version = "~> 2.3"
 7    }
 8    azurerm = {
 9      source  = "hashicorp/azurerm"
10      version = "~> 4.27"
11    }
12    modtm = {
13      source  = "azure/modtm"
14      version = "~> 0.3"
15    }
16    random = {
17      source  = "hashicorp/random"
18      version = "~> 3.7"
19    }
20    time = {
21      source  = "hashicorp/time"
22      version = "~> 0.13"
23    }
24    tls = {
25      source  = "hashicorp/tls"
26      version = "~> 4.1"
27    }
28  }
29}
30
31provider "azurerm" {
32  features {
33  }
34}

Conclusion

Congratulations on successfully implementing a solution using Azure Verified Modules! You were able to build out our sample architecture using module documentation and taking advantage of features like standard interfaces and pre-defined defaults to simplify the development experience.

Note

This was a long exercise and mistakes can happen. If you’re getting errors or a resource is incomplete and you want to see the final main.tf, expand the following code block to see the full file.

βž• Expand Code
  1module "avm-res-resources-resourcegroup" {
  2  source  = "Azure/avm-res-resources-resourcegroup/azurerm"
  3  version = "0.2.1"
  4
  5  name = "${var.name_prefix}-rg"
  6  location = var.location
  7  tags = var.tags
  8}
  9
 10module "avm-res-operationalinsights-workspace" {
 11  source  = "Azure/avm-res-operationalinsights-workspace/azurerm"
 12  version = "0.4.2"
 13
 14  enable_telemetry                          = true
 15  location                                  = module.avm-res-resources-resourcegroup.resource.location
 16  resource_group_name                       = module.avm-res-resources-resourcegroup.name
 17  name                                      = "${var.name_prefix}-law"
 18  log_analytics_workspace_retention_in_days = 30
 19  log_analytics_workspace_sku               = "PerGB2018"
 20}
 21
 22data "azurerm_client_config" "this" {}
 23
 24resource "random_string" "name_suffix" {
 25  length  = 4
 26  special = false
 27  upper   = false
 28}
 29
 30module "avm-res-keyvault-vault" {
 31  source  = "Azure/avm-res-keyvault-vault/azurerm"
 32  version = "0.10.0"
 33
 34  enable_telemetry    = true
 35  location            = module.avm-res-resources-resourcegroup.resource.location
 36  resource_group_name = module.avm-res-resources-resourcegroup.name
 37  name                = "${var.name_prefix}-kv-${random_string.name_suffix.result}"
 38  tenant_id           = data.azurerm_client_config.this.tenant_id
 39  network_acls        = null
 40
 41  diagnostic_settings = {
 42    to_la = {
 43      name                  = "${var.name_prefix}-kv-diags"
 44      workspace_resource_id = module.avm-res-operationalinsights-workspace.resource_id
 45    }
 46  }
 47
 48  role_assignments = {
 49    deployment_user_kv_admin = {
 50      role_definition_id_or_name = "Key Vault Administrator"
 51      principal_id               = data.azurerm_client_config.this.object_id
 52    }
 53  }
 54}
 55
 56module "avm-res-network-natgateway" {
 57  source  = "Azure/avm-res-network-natgateway/azurerm"
 58  version = "0.2.1"
 59
 60  name                = "${var.name_prefix}-natgw"
 61  enable_telemetry    = true
 62  location            = module.avm-res-resources-resourcegroup.resource.location
 63  resource_group_name = module.avm-res-resources-resourcegroup.name
 64
 65  public_ips = {
 66    public_ip_1 = {
 67      name = "${var.name_prefix}-natgw-pip"
 68    }
 69  }
 70}
 71
 72module "avm-res-network-virtualnetwork" {
 73  source  = "Azure/avm-res-network-virtualnetwork/azurerm"
 74  version = "0.8.1"
 75
 76  resource_group_name = module.avm-res-resources-resourcegroup.name
 77  location            = module.avm-res-resources-resourcegroup.resource.location
 78  name                = "${var.name_prefix}-vnet"
 79
 80  address_space = [var.virtual_network_cidr]
 81
 82  subnets = {
 83    subnet0 = {
 84      name                            = "${var.name_prefix}-vm-subnet"
 85      default_outbound_access_enabled = false
 86      address_prefixes = [cidrsubnet(var.virtual_network_cidr, 1, 0)]
 87      nat_gateway = {
 88        id = module.avm-res-network-natgateway.resource_id
 89      }
 90      network_security_group = {
 91        id = module.avm-res-network-networksecuritygroup.resource_id
 92      }
 93    }
 94    bastion = {
 95      name                            = "AzureBastionSubnet"
 96      default_outbound_access_enabled = false
 97      address_prefixes = [cidrsubnet(var.virtual_network_cidr, 1, 1)]
 98    }
 99  }
100
101  diagnostic_settings = {
102    sendToLogAnalytics = {
103      name                           = "${var.name_prefix}-vnet-diagnostic"
104      workspace_resource_id          = module.avm-res-operationalinsights-workspace.resource_id
105      log_analytics_destination_type = "Dedicated"
106    }
107  }
108}
109
110module "avm-res-network-bastionhost" {
111  source  = "Azure/avm-res-network-bastionhost/azurerm"
112  version = "0.7.2"
113
114  name                = "${var.name_prefix}-bastion"
115  resource_group_name = module.avm-res-resources-resourcegroup.name
116  location            = module.avm-res-resources-resourcegroup.resource.location
117  ip_configuration = {
118    subnet_id = module.avm-res-network-virtualnetwork.subnets["bastion"].resource_id
119  }
120
121  diagnostic_settings = {
122    sendToLogAnalytics = {
123      name                           = "${var.name_prefix}-bastion-diagnostic"
124      workspace_resource_id          = module.avm-res-operationalinsights-workspace.resource_id
125      log_analytics_destination_type = "Dedicated"
126    }
127  }
128}
129
130module "avm-res-network-networksecuritygroup" {
131  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
132  version = "0.4.0"
133  resource_group_name = module.avm-res-resources-resourcegroup.name
134  name                = "${var.name_prefix}-vm-subnet-nsg"
135  location            = module.avm-res-resources-resourcegroup.resource.location
136
137  security_rules = {
138    "rule01" = {
139      name                       = "${var.name_prefix}-ssh"
140      access                     = "Allow"
141      destination_address_prefix = "*"
142      destination_port_ranges    = ["22"]
143      direction                  = "Inbound"
144      priority                   = 200
145      protocol                   = "Tcp"
146      source_address_prefix      = "*"
147      source_port_range          = "*"
148    }
149  }
150}
151
152module "avm-res-compute-virtualmachine" {
153  source  = "Azure/avm-res-compute-virtualmachine/azurerm"
154  version = "0.19.1"
155
156  enable_telemetry    = true
157  location            = module.avm-res-resources-resourcegroup.resource.location
158  resource_group_name = module.avm-res-resources-resourcegroup.name
159  name                = "${var.name_prefix}-vm"
160  os_type             = "Linux"
161  sku_size            = "Standard_D2s_v5"
162  zone                = 1
163
164  source_image_reference = {
165    publisher = "Canonical"
166    offer     = "0001-com-ubuntu-server-focal"
167    sku       = "20_04-lts-gen2"
168    version   = "latest"
169  }
170
171  network_interfaces = {
172    network_interface_1 = {
173      name = "${var.name_prefix}-nic-${random_string.name_suffix.result}"
174      ip_configurations = {
175        ip_configuration_1 = {
176          name                          = "${var.name_prefix}-ipconfig-${random_string.name_suffix.result}"
177          private_ip_subnet_resource_id = module.avm-res-network-virtualnetwork.subnets["subnet0"].resource_id
178        }
179      }
180    }
181  }
182
183  diagnostic_settings = {
184    sendToLogAnalytics = {
185      name                           = "${var.name_prefix}-vm-diagnostic"
186      workspace_resource_id          = module.avm-res-operationalinsights-workspace.resource_id
187      log_analytics_destination_type = "Dedicated"
188    }
189  }
190}

AVM modules provide several key advantages over writing raw Terraform templates:

  1. Simplified Resource Configuration: AVM modules handle much of the complex configuration work behind the scenes
  2. Built-in Recommended Practices: The modules implement many of Microsoft’s recommended practices by default
  3. Consistent Outputs: Each module exposes a consistent set of outputs that can be easily referenced
  4. Reduced Boilerplate Code: What would normally require hundreds of lines of Terraform code can be accomplished in a fraction of the space

As you continue your journey with Azure and AVM, remember that this approach can be applied to more complex architectures as well. The modular nature of AVM allows you to mix and match components to build solutions that meet your specific needs while adhering to Microsoft’s Well-Architected Framework.

By using AVM modules as building blocks, you can focus more on your solution architecture and less on the intricacies of individual resource configurations, ultimately leading to faster development cycles and more reliable deployments.

Additional exercises

For additional learning, it can be helpful to experiment with modifying this solution. Here are some ideas you can try if you have time and would like to experiment further.

  1. Use the managed_identities interface to add a system assigned managed identity to the virtual machine and give it Key Vault Administrator rights on the Key Vault.
  2. Use the tags interface to assign tags directly to one or more resources.
  3. Add an Azure Monitoring Agent extension to the virtual machine resource.
  4. Add additional inputs like VM sku to your module to make it more customizable. Be sure to update the code and tfvars files to match.

Clean up your environment

Once you have completed this set of exercises, it is a good idea to clean up your resources to avoid incurring costs for them. This can be done typing terraform destroy -var-file=development.tfvars and entering yes when prompted.

Solution Development

Considerations and steps of Solution Development

  • Decide on the IaC language (Bicep or Terraform)
  • Decide on the module sourcing method (public registry, private registry, inner-sourcing)
  • Decide on the orchestration method (template or pipeline)
  • Identify the resources needed for the solution (are they all available in AVM?)
  • Implement, validate, deploy, test the solution

Questions to cover on this page

  • Pick a realistically complex solution and demonstrate how to build it using AVM modules
  • Best practices for coding (link to official language specific guidance AND AVM specs where/if applicable)
  • Best practices for input and output parameters

Next steps

To be covered in separate, future articles.

To make this solution enterprise-ready, you need to consider the following:

  • Deploy with DevOps tools and practices (e.g., CI/CD in Azure DevOps, GitHub Actions, etc.)
  • Deploy into Azure Landing Zones (ALZ)
  • Make sure the solution follows the recommendations of the Well-Architected Framework (WAF) and it’s compliant with and integrates into your organization’s policies and standards, e.g.:
    • Security & Identity (e.g., RBAC, Entra ID, service principals, secrets management, MFA, etc.)
    • Networking (e.g., Azure Firewall, NSGs, etc.)
    • Monitoring (e.g., Azure Monitor, Log Analytics, etc.)
    • Cost management (e.g., Azure Cost Management, budgets, etc.)
    • Governance (e.g., Azure Policy, etc.)

Other recommendations

  • Don’t use latest, but a specific version of the module
  • Don’t expose secrets in output parameters/command line/logs/etc.
  • Don’t use hard-coded values, but use parameters and variables

Quickstart Guide

This QuickStart guide offers step-by-step instructions for integrating Azure Verified Modules (AVM) into your solutions. It includes the initial setup, essential tools, and configurations required to deploy and manage your Azure resources efficiently using AVM.

The AVM Key Vault resource module, used as an example in this chapter, simplifies the deployment and management of Azure Key Vaults, ensuring secure storage and access to your secrets, keys, and certificates.

Leveraging Azure Verified Modules

Using AVM ensures that your infrastructure-as-code deployments follow Microsoft’s best practices and guidelines, providing a consistent and reliable foundation for your cloud solutions. AVM helps accelerate your development process, reduce the risk of misconfigurations, and enhance the security and compliance of your applications.

Using default values

The default values provided by AVM are generally safe, as they follow best practices and ensure a secure and reliable setup. However, it is important to review these values to ensure they meet your specific requirements and compliance needs. Customizing the default values may be necessary to align with your organization’s policies and the specific needs of your solution.

Exploring examples and module features

You can find examples and detailed documentation for each AVM module in their respective code repository’s README.MD file, which details features, input parameters, and outputs. The module’s documentation also provides comprehensive usage examples, covering various scenarios and configurations. Additionally, you can explore the module’s source code repository. This information will help you understand the full capabilities of the module and how to effectively integrate it into your solutions.

Subsections of Quickstart

Bicep Quickstart Guide

Introduction

This guide explains how to use an Azure Verified Modules (AVM) in your Bicep workflow. By leveraging AVM modules, you can rapidly deploy and manage Azure infrastructure without having to write extensive code from scratch.

In this guide, you will deploy a Key Vault resource and a Personal Access Token as a secret.

This article is intended for a typical ‘infra-dev’ user (cloud infrastructure professional) who has a basic understanding of Azure and Bicep but is new to Azure Verified Modules and wants to learn how to deploy a module in the easiest way using AVM.

For additional Bicep learning resources use the Bicep documentation on the Microsoft Learn platform, or leverage the Fundamentals of Bicep learning path.

Prerequisites

You will need the following tools and components to complete this guide:

Make sure you have these tools set up before proceeding.

Module Discovery

Find your module

In this scenario, you need to deploy a Key Vault resource and some of its child resources, such as a secret. Let’s find the AVM module that will help us achieve this.

There are two primary ways for locating published Bicep Azure Verified Modules:

  • Option 1 (preferred): Using IntelliSense in the Bicep extension of Visual Studio Code, and
  • Option 2: browsing the AVM Bicep module index.

Option 1: Use the Bicep Extension in VS Code

  1. In VS Code, create a new file called main.bicep.
  2. Start typing module, then give your module a symbolic name, such as myModule.
  3. Use IntelliSense to select br/public.
  4. The list of all AVM modules published in the Bicep Public Registry will show up. Use this to explore the published modules.
    Note

    The Bicep VSCode extension is reading metadata through this JSON file. All modules are added to this file, as part of the publication process. This lists all the modules marked as Published or Orphaned on the AVM Bicep module index pages.

  5. Select the module you want to use and the version you want to deploy. Note how you can type full or partial module names to filter the list.
  6. Right click on the module’s path and select Go to definition or hit F12 to see the module’s source code. You can toggle between the Bicep and the JSON view.
  7. Hover over the module’s symbolic name to view its documentation URL. By clicking on it, you will be directed to the module’s GitHub folder in the bicep-registry-modules (BRM) repository. There, you can access the source code and documentation, as illustrated below.

Option 2: Use the AVM Bicep Module Index

Searching the Azure Verified Modules indexes is the most complete way to discover published as well as planned (proposed) modules. As shown in the video above, use the following steps to locate a specific module on the AVM website:

  1. Open the AVM website in your favorite web browser: https://aka.ms/avm.
  2. Expand the Module Indexes menu item and select the Bicep sub-menu item.
  3. Select the menu item for the module type you are searching for: Resource, Pattern, or Utility.
    Note

    Since the Key Vault module used as an example in this guide is published as an AVM resource module, it can be found under the resource modules section in the AVM Bicep module index.

  4. A detailed description of module classification types can be found under the related section here.
  5. Select the Published modules link from the table of contents at the top of the page.
  6. Use the in-page search feature of your browser. In most Windows browsers you can access it using the CTRL + F keyboard shortcut.
  7. Enter a search term to find the module you are looking for - e.g., Key Vault.
  8. Move through the search results until you locate the desired module. If you are unable to find a published module, return to the table of contents and expand the All modules link to search both published and proposed modules - i.e., modules that are planned, likely in development but not published yet.
  9. After finding the desired module, click on the module’s name. This link will lead you to the module’s folder in the bicep-registry-modules (BRM) repository, where the module’s source code and documentation can be found, including usage examples.

Module details and examples

In the module’s documentation, you can find detailed information about the module’s functionality, components, input parameters, outputs and more. The documentation also provides comprehensive usage examples, covering various scenarios and configurations.

Explore the Key Vault module’s documentation for usage examples and to understand its functionality, input parameters, and outputs.

  1. Note the mandatory and optional parameters in the Parameters section.

  2. Review the Usage examples section. AVM modules include multiple tests that can be found under the tests folder. These tests are also used as the basis of the usage examples ensuring they are always up-to-date and deployable.

In this example, you will deploy a secret in a new Key Vault instance with minimal input. AVM provides default parameter values with security and reliability being core principles. These settings apply the recommendations of the Well Architected Framework where possible and appropriate.

Note how Example 2 does most of what you need to achieve.

Create your new solution using AVM

In this section, you will develop a Bicep template that references the AVM Key Vault module and its child resources and features. These include secret and role based access control configurations that grant permissions to a user.

  1. Start VSCode (make sure the Bicep extension is installed) and open a folder in which you want to work.
  2. Create a main.bicep and a dev.bicepparam file, which will hold parameters for your Key Vault deployment.
  3. Copy the content below into your main.bicep file. We have included comments to distinguish between the two different occurrences of the names attribute.
module myKeyVault 'br/public:avm/res/key-vault/vault:0.11.0' = {
  name: // the name of the module's deployment
  params: {
    name: '<keyVaultName>' // the name of the Key Vault instance - length and character limits apply
  }
}
Note

For Azure Key Vaults, the name must be globally unique. When you deploy the Key Vault, ensure you select a name that is alphanumeric, twenty-four characters or less, and unique enough to ensure no one else has used the name for their Key Vault. If the name has been previously taken, you will get an error.

After setting the values for the required properties, the module can be deployed. This minimal configuration automatically applies the security and reliability recommendations of the Well Architected Framework where possible and appropriate. These settings can be overridden if needed.

Bicep-specific configuration

It is recommended to create a bicepconfig.json file, and enable use-recent-module-versions, which warns you to use the latest available version of the AVM module.

// This is a Bicep configuration file. It can be used to control how Bicep operates and to customize
// validation settings for the Bicep linter. The linter relies on these settings when evaluating your
// Bicep files for best practices. For further information, please refer to the official documentation at:
// https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-config
{
  "analyzers": {
    "core": {
      "rules": {
        "use-recent-module-versions": {
          "level": "warning",
          "message": "The module version is outdated. Please consider updating to the latest version."
        }
      }
    }
  }
}

Define the Key Vault instance

In this scenario - and every other real-world setup - there is more that you need to configure. You can open the module’s documentation by hovering over its symbolic name to see all of the module’s capabilities - including supported parameters.

Note

The Bicep extension facilitates code-completion, enabling you to easily locate and utilize the Azure Verified Module. This feature also provides the necessary properties for a module, allowing you to begin typing and leverage IntelliSense for completion.

  1. Add parameters and values to the main.bicep file to customize your configuration. These parameters are used for passing in the Key Vault name and enabling purge protection. You might not want to enable the latter in a non-production environment, as it makes it harder to delete and recreate resources.

The main.bicep file will now look like this:

// the scope, the deployment deploys resources to
targetScope = 'resourceGroup'

// parameters and default values
param keyVaultName string

@description('Disable for development deployments.')
param enablePurgeProtection bool = true

// the resources to deploy
module myKeyVault 'br/public:avm/res/key-vault/vault:0.11.0' = {
  name: 'key-vault-deployment'
  params: {
    name: keyVaultName
    enablePurgeProtection: enablePurgeProtection
    // more properties are not needed, as AVM provides default values
  }
}

Note that the Key Vault instance will be deployed within a resource group scope in our example.

  1. Create a dev.bicepparam file (this is optional) and set parameter values for your environment. You can now pass these values by referencing this file at the time of deployment (using PowerShell or Azure CLI).
using 'main.bicep'

// environment specific values
param keyVaultName = '<keyVaultName>'
param enablePurgeProtection = false

Create a secret and set permissions

Add a secret to the Key Vault instance and grant permissions to a user to work with the secret. Sample role assignments can be found in Example 3: Using large parameter set. See Parameter: roleAssignments for a list of pre-defined roles that you can reference by name instead of a GUID. This is a key benefit of using AVM, as the code is easy to read and increases the maintainability.

You can also leverage User-defined data types and simplify the parameterization of the modules instead of guessing or looking up parameters. Therefore, first import UDTs from the Key Vault and common types module and leverage the UDTs in your Bicep and parameter files.

For a role assignment, the principal ID is needed, that will be granted a role (specified by its name) on the resource. Your own ID can be found out with az ad signed-in-user show --query id.

// the scope, the deployment deploys resources to
targetScope = 'resourceGroup'

// parameters and default values
param keyVaultName string
// the PAT token is a secret and should not be stored in the Bicep(parameter) file.
// It can be passed via the commandline, if you don't use a parameter file.
@secure()
param patToken string = newGuid()

@description('Enabled by default. Disable for development deployments')
param enablePurgeProtection bool = true

import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.4.0'
// the role assignments are optional in the Key Vault module
param roleAssignments roleAssignmentType[]?

// the resources to deploy
module myKeyVault 'br/public:avm/res/key-vault/vault:0.11.0' = {
  name: 'key-vault-deployment'
  params: {
    name: keyVaultName
    enablePurgeProtection: enablePurgeProtection
    secrets: [
      {
        name: 'PAT'
        value: patToken
      }
    ]
    roleAssignments: roleAssignments
  }
}

The secrets parameter references a UDT (User-defined data type) that is part of the Key Vault module and enables code completion for easy usage. There is no need to look up what attributes the secret object might have. Start typing and tab-complete what you need from the content offered by the Bicep extension’s integration with AVM.

The bicep parameter file now looks like this:

// reference to the Bicep file to set the context
using 'main.bicep'

// environment specific values
param keyVaultName = '<keyVaultName>'
param enablePurgeProtection = false
// for security reasons, the secret value must not be stored in this file.
// You can change it later in the deployed Key Vault instance, where you also renew it after expiration.

param roleAssignments = [
  {
    principalId: '<principalId>'
    // using the name of the role instead of looking up the GUID (which can also be used)
    roleDefinitionIdOrName: 'Key Vault Secrets Officer'
  }
]
Note

The display names for roleDefinitionIdOrName can be acquired the following two ways:

  • From the parameters section of the module’s documentation.
  • From the builtInRoleNames variable in the module’s source code. To get there, hit F12 while the cursor is on the part of the module path starting with br/public:.

Boost your development with VS Code IntelliSense

Leverage the IntelliSense feature in VS Code to speed up your development process. IntelliSense provides code completion, possible parameter values and structure. It helps you write code more efficiently by providing context-aware suggestions as you type.

Here is how quickly you can deliver the solution detailed in this section:

Deploy your solution

Now that your template and parameter file is ready, you can deploy your solution to Azure. Use PowerShell or the Azure CLI to deploy your solution.

Deploy with
# Log in to Azure
Connect-AzAccount

# Select your subscription
Set-AzContext -SubscriptionId '<subscriptionId>'

# Deploy a resource group
New-AzResourceGroup -Name 'avm-quickstart-rg' -Location 'germanywestcentral'

# Invoke your deployment
New-AzResourceGroupDeployment -DeploymentName 'avm-quickstart-deployment' -ResourceGroupName 'avm-quickstart-rg' -TemplateParameterFile 'dev.bicepparam' -TemplateFile 'main.bicep'
# Log in to Azure
az login

# Select your subscription
az account set --subscription '<subscriptionId>'

# Deploy a resource group
az group create --name 'avm-quickstart-rg' --location 'germanywestcentral'

# Invoke your deployment
az deployment group create --name 'avm-quickstart' --resource-group 'avm-quickstart-rg' --template-file 'main.bicep' --parameters 'dev.bicepparam'

Use the Azure portal, Azure PowerShell, or the Azure CLI to verify that the Key Vault instance and secret have been successfully created with the correct configuration.

Clean up your environment

When you are ready, you can remove the infrastructure deployed in this example. The following commands will remove all resources created by your deployment:

Clean up with
# Delete the resource group
Remove-AzResourceGroup -Name "avm-quickstart-rg" -Force

# Purge the Key Vault
Remove-AzKeyVault -VaultName "<keyVaultName>" -Location "germanywestcentral" -InRemovedState -Force
# Delete the resource group
az group delete --name 'avm-quickstart-rg' --yes --no-wait

# Purge the Key Vault
az keyvault purge --name '<keyVaultName>' --no-wait

Congratulations, you have successfully leveraged an AVM Bicep module to deploy resources in Azure!

Tip

We welcome your contributions and feedback to help us improve the AVM modules and the overall experience for the community!

Next Steps

For developing a more advanced solution, please see the lab titled “Introduction to using Azure Verified Modules for Bicep”.

Terraform Quickstart Guide

Introduction

This guide explains how to use an Azure Verified Modules (AVM) in your Terraform workflow. With AVM modules, you can quickly deploy and manage Azure infrastructure without writing extensive code from scratch.

In this guide, you will deploy a Key Vault resource and generate and store a key.

This article is intended for a typical ‘infra-dev’ user (cloud infrastructure professional) who is new to Azure Verified Modules and wants to learn how to deploy a module in the easiest way using AVM. The user has a basic understanding of Azure and Terraform.

For additional Terraform resources, try a tutorial on the HashiCorp website or study the detailed documentation.

Prerequisites

You will need the following tools and components to complete this guide:

Before you begin, ensure you have these tools installed in your development environment.

Module Discovery

Find your module

In this scenario, you need to deploy a Key Vault resource and some of its child resources, such as a key. Let’s find the AVM module that will help us achieve this.

There are two primary ways for locating published Terraform Azure Verified Modules:

Use the Terraform Registry

The easiest way to find published AVM Terraform modules is by searching the Terraform Registry. Follow these steps to locate a specific module, as shown in the video above.

  • Use your web browser to go to the HashiCorp Terraform Registry
  • In the search bar at the top of the screen type avm. Optionally, append additional search terms to narrow the search results. (e.g., avm key vault for AVM modules with Key Vault in the name.)
  • Select see all to display the full list of published modules matching your search criteria.
  • Find the module you wish to use and select it from the search results.
Note

It is possible to discover other unofficial modules with avm in the name using this search method. Look for the Partner tag in the module title to determine if the module is part of the official set.

Use the AVM Terraform Module Index

Searching the Azure Verified Modules indexes is the most complete way to discover published as well as planned modules - shown as proposed. As presented in the video above, use the following steps to locate a specific module on the AVM website:

  • Use your web browser to open the AVM website at https://aka.ms/avm.
  • Expand the Module Indexes menu item and select the Terraform sub-menu item.
  • Select the menu item for the module type you are searching for: Resource, Pattern, or Utility.
    Note

    Since the Key Vault module used as an example in this guide is published as an AVM resource module, it can be found under the resource modules section in the AVM Terraform module index.

  • A detailed description of each module classification type can be found under the related section here.
  • Select the Published modules link from the table of contents at the top of the page.
  • Use the in-page search feature of your browser (in most Windows browsers you can access it using the CTRL + F keyboard shortcut).
  • Enter a search term to find the module you are looking for - e.g., Key Vault.
  • Move through the search results until you locate the desired module. If you are unable to find a published module, return to the table of contents and expand the All modules link to search both published and proposed modules - i.e., modules that are planned, likely in development but not published yet.
  • After finding the desired module, click on the module’s name. This link will lead you to the official HashiCorp Terraform Registry page for the module where you can find the module’s documentation and examples.

Module details and examples

Once you have identified the AVM module in the Terraform Registry you can find detailed information about the module’s functionality, components, input parameters, outputs and more. The documentation also provides comprehensive usage examples, covering various scenarios and configurations.

Explore the Key Vault module’s documentation and usage examples to understand its functionality, input variables, and outputs.

  • Note the Examples drop-down list and explore each example
  • Review the Readme tab to see module provider minimums, a list of resources and data sources used by the module, a nicely formatted version of the inputs and outputs, and a reference to any submodules that may be called.
  • Explore the Inputs tab and observe how each input has a detailed description and a type definition for you to use when adding input values to your module configuration.
  • Explore the Outputs tab and review each of the outputs that are exported by the AVM module for use by other modules in your deployment.
  • Finally, review the Resources tab to get a better understanding of the resources defined in the module.

In this example, you will deploy a secret in a new Key Vault instance without needing to provide other parameters. The AVM Key Vault resource module provides these capabilities and does so with security and reliability being core principles. The default settings of the module also apply the recommendations of the Well Architected Framework where possible and appropriate.

Note how the create-key example seems to do what you need to achieve.

Create your new solution using AVM

Now that you have found the module details, you can use the content from the Terraform Registry to speed up your development in the following ways:

  1. Option 1: Create a solution using AVM module examples: duplicate a module example and edit it for your needs. This is useful if you are starting without any existing infrastructure and need to create supporting resources like resource groups as part of your deployment.
  2. Option 2: Create a solution by changing the AVM module input values: add the AVM module to an existing solution that already includes other resources. This method requires some knowledge of the resource(s) being deployed so that you can make choices about optional features configured in your solution’s version of the module.

Each deployment method includes a section below so that you can choose the method which best fits your needs.

Note

For Azure Key Vaults, the name must be globally unique. When you deploy the Key Vault, ensure you select a name that is alphanumeric, twenty-four characters or less, and unique enough to ensure no one else has used the name for their Key Vault. If the name has been used previously, you will get an error.

Option 1: Create a solution using AVM module examples

Leverage the following steps as a template for how to leverage examples for bootstrapping your new solution code. The Key Vault resource module is used here as an example, but in practice you may choose any module that applies to your scenario.

  • Locate and select the Examples drop down menu in the middle of the Key Vault module page.
  • From the drop-down list select an example whose name most closely aligns with your scenario - e.g., create-key.
  • When the example page loads, read the example description to determine if this is the desired example. If it is not, return to the module main page, and select a different example until you are satisfied that the example covers the scenario you are trying to deploy. If you are unable to find a suitable example, leverage the last two steps in the option 2 instructions to modify the inputs of the selected example to match your requirements.
  • Scroll to the code block for the example and select the Copy button on the top right of the block to copy the content to the clipboard.
βž• Click here to copy the sample code from the video.
provider "azurerm" {
  features {}
}

terraform {
  required_version = "~> 1.9"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.71"
    }
    http = {
      source  = "hashicorp/http"
      version = "~> 3.4"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.5"
    }
  }
}

module "regions" {
  source  = "Azure/avm-utl-regions/azurerm"
  version = "0.1.0"
}

# This allows us to randomize the region for the resource group.
resource "random_integer" "region_index" {
  max = length(module.regions.regions) - 1
  min = 0
}

# This ensures you have unique CAF compliant names for our resources.
module "naming" {
  source  = "Azure/naming/azurerm"
  version = "0.3.0"
}

resource "azurerm_resource_group" "this" {
  location = module.regions.regions[random_integer.region_index.result].name
  name     = module.naming.resource_group.name_unique
}

# Get current IP address for use in KV firewall rules
data "http" "ip" {
  url = "https://api.ipify.org/"
  retry {
    attempts     = 5
    max_delay_ms = 1000
    min_delay_ms = 500
  }
}

data "azurerm_client_config" "current" {}

module "key_vault" {
  source                        = "Azure/avm-res-keyvault-vault/azurerm"
  name                          = module.naming.key_vault.name_unique
  location                      = azurerm_resource_group.this.location
  enable_telemetry              = var.enable_telemetry
  resource_group_name           = azurerm_resource_group.this.name
  tenant_id                     = data.azurerm_client_config.current.tenant_id
  public_network_access_enabled = true
  keys = {
    cmk_for_storage_account = {
      key_opts = [
        "decrypt",
        "encrypt",
        "sign",
        "unwrapKey",
        "verify",
        "wrapKey"
      ]
      key_type: "RSA"
      name     = "cmk-for-storage-account"
      key_size = 2048
    }
  }
  role_assignments = {
    deployment_user_kv_admin = {
      role_definition_id_or_name = "Key Vault Administrator"
      principal_id               = data.azurerm_client_config.current.object_id
    }
  }
  wait_for_rbac_before_key_operations = {
    create = "60s"
  }
  network_acls = {
    bypass   = "AzureServices"
    ip_rules = ["${data.http.ip.response_body}/32"]
  }
}
  • In your IDE - Visual Studio Code in our example - create the main.tf file for your new solution.

  • Paste the content from the clipboard into main.tf.

  • AVM examples frequently use naming and/or region selection AVM utility modules to generate deployment region and/or naming values as well as any default values for required fields. If you want to use a specific region name or other custom resource values, remove the existing region and naming module calls and replace example input values with the new desired custom input values.

  • Once supporting resources such as resource groups have been modified, locate the module call for the AVM module - i.e., module "keyvault".

  • AVM module examples use dot notation for a relative reference that is useful during module testing. However, you will need to replace the relative reference with a source reference that points to the Terraform Registry source location. In most cases, this source reference has been left as a comment in the module example to simplify replacing the existing source dot reference. Perform the following two actions to update the source:

    • Delete the existing source definition that uses a dot reference - i.e., source = "../../".
    • Uncomment the Terraform Registry source reference by deleting the # sign at the start of the commented source line - i.e., source = "Azure/avm-res-keyvault-vault/azurerm".
    Note

    If the module example does not include a commented Terraform Registry source reference, you will need to copy it from the module’s main documentation page. Use the following steps to do so:

    • Use the breadcrumbs to leave the example documentation and return to the module’s primary Terraform Registry documentation page.
    • Locate the Provision Instructions box on the right side of the module’s Terraform Registry page in your web browser.
    • Select the second line that starts with source = from the code block - e.g., source = "Azure/avm-res-keyvault-vault/azurerm". Copy it onto the clipboard.
    • Return to your code solution and Paste the clipboard’s content where you previously deleted the source dot reference - e.g., source = "../../".
  • AVM module examples use a variable to enable or disable the telemetry collection. Update the enable_telemetry input value to true or false. - e.g. enable_telemetry = true

  • Save your main.tf file changes and then proceed to the guide section for running your solution code.

Option 2: Create a solution by changing the AVM module input values

Click here to copy the sample code from the video.
module "avm-res-keyvault-vault" {
  source                        = "Azure/avm-res-keyvault-vault/azurerm"
  version                       = "0.9.1"
  name                          = "<custom_name_here>"
  resource_group_name           = azurerm_resource_group.this.name
  location                      = azurerm_resource_group.this.location
  tenant_id                     = data.azurerm_client_config.this.tenant_id

  keys = {
    cmk_for_storage_account = {
      key_opts = [
        "decrypt",
        "encrypt",
        "sign",
        "unwrapKey",
        "verify",
        "wrapKey"
      ]
      key_type: "RSA"
      name     = "cmk-for-storage-account"
      key_size = 2048
    }
  }
  role_assignments = {
    deployment_user_kv_admin = {
      role_definition_id_or_name = "Key Vault Administrator"
      principal_id               = data.azurerm_client_config.current.object_id
    }
  }
  wait_for_rbac_before_key_operations = {
    create = "60s"
  }
}

Use the following steps as a guide for the custom implementation of an AVM Module in your solution code. This instruction path assumes that you have an existing Terraform file that you want to add the AVM module to.

  • Locate the Provision Instructions box on the right side of the module’s Terraform Registry page in your web browser.
  • Select the module template code from the code block and Copy it onto the clipboard.
  • Switch to your IDE and Paste the contents of the clipboard into your solution’s .tf Terraform file - main.tf in our example.
  • Return to the module’s Terraform Registry page in the browser and select the Inputs tab.
  • Review each input and add the inputs with the desired target value to the solution’s code - i.e., name = "custom_name".
  • Once you are satisfied that you have included all required inputs and any optional inputs, Save your file and continue to the next section.

Deploy your solution

After completing your solution development, you can move to the deployment stage. Follow these steps for a basic Terraform workflow:

  • Open the command line and login to Azure using the Azure cli

    az login
  • If your account has access to multiple tenants, you may need to modify the command to az login --tenant <tenant id> where “<tenant id>” is the guid for the target tenant.

  • After logging in, select the target subscription from the list of subscriptions that you have access to.

  • Change the path to the directory where your completed terraform solution files reside.

    Note

    Many AVM modules depend on the AzureRM 4.0 Terraform provider which mandates that a subscription id is configured. If you receive an error indicating that subscription_id is a required provider property, you will need to set a subscription id value for the provider. For Unix based systems (Linux or MacOS) you can configure this by running export ARM_SUBSCRIPTION_ID=<your subscription guid> on the command line. On Microsoft Windows, you can perform the same operation by running set ARM_SUBSCRIPTION_ID="<your subscription guid>" from the Windows command prompt or by running $env:ARM_SUBSCRIPTION_ID="<your subscription guid>" from a powershell prompt. Replace the “<your subscription id>” notation in each command with your Azure subscription’s unique id value.

  • Initialize your Terraform project. This command downloads the necessary providers and modules to the working directory.

    terraform init
  • Before applying the configuration, it is good practice to validate it to ensure there are no syntax errors.

    terraform validate
  • Create a deployment plan. This step shows what actions Terraform will take to reach the desired state defined in your configuration.

    terraform plan
  • Review the plan to ensure that only the desired actions are in the plan output.

  • Apply the configuration and create the resources defined in your configuration file. This command will prompt you to confirm the deployment prior to making changes. Type yes to create your solution’s infrastructure.

    terraform apply
    Info

    If you are confident in your changes, you can add the -auto-approve switch to bypass manual approval: terraform apply -auto-approve

  • Once the deployment completes, validate that the infrastructure is configured as desired.

    Info

    A local terraform.tfstate file and a state backup file have been created during the deployment. The use of local state is acceptable for small temporary configurations, but production or long-lived installations should use a remote state configuration where possible. Configuring remote state is out of scope for this guide, but you can find details on using an Azure storage account for this purpose in the Microsoft Learn documentation.

Clean up your environment

When you are ready, you can remove the infrastructure deployed in this example. Use the following command to delete all resources created by your deployment:

terraform destroy
Note

Most Key Vault deployment examples activate soft-delete functionality as a default. The terraform destroy command will remove the Key Vault resource but does not purge a soft-deleted vault. You may encounter errors if you attempt to re-deploy a Key Vault with the same name during the soft-delete retention window. If you wish to purge the soft-delete for this example you can run az keyvault purge -n <keyVaultName> -l <regionName> using the Azure CLI, or Remove-AzKeyVault -VaultName "<keyVaultName>" -Location "<regionName>" -InRemovedState using Azure PowerShell.

Congratulations, you have successfully leveraged Terraform and AVM to deploy resources in Azure!

Tip

We welcome your contributions and feedback to help us improve the AVM modules and the overall experience for the community!

Next Steps

For developing a more advanced solution, please see the lab titled “Introduction to using Azure Verified Modules for Terraform”.

Subsections of Specifications & Definitions

Module Specifications

This section documents all the specifications for Azure Verified Modules (AVM) and their respective IaC languages.

Specifications by IaC Language

CategoryBicepTerraform
ResourcePatternUtilityResourcePatternUtility
Contribution/Support988988
Telemetry434222
Naming/Composition2518917127
CodeStyle222292929
Inputs/Outputs141110865
Testing14131310109
Documentation555444
Release/Publishing555444
Summary786556837568

What changed recently?

See what specifications changed in the last 30 days...

#IDLast Modified (UTC)Git HistoryLast Commit
1BCPFR62026-03-20 11:06:37All Commits2f883f7
2BCPFR72026-03-20 11:06:37All Commits2f883f7
3BCPNFR242026-03-20 11:06:37All Commits2f883f7

How to navigate the specifications?

The “Module Specifications” section uses tags to dynamically render content based on the selected attributes, such as the IaC language, module classification, category, severity and more. The tags are defined in header of each specification page.

To make it easier for module owners and contributors to navigate the documentation, the specifications are grouped to distinct pages by the IaC language (Bicep | Terraform) and module classification ( resource | pattern | utility). The specifications on each page are further ordered by the category (e.g., Composition, CodeStyle, Testing, etc.), severity of the requirements (MUST | SHOULD | MAY) and at what stage of the module’s lifecycle the specification is typically applicable (Initial | BAU | EOL).

To find what you need, simply decide which IaC language you’d like develop in and what classification your module falls under, then navigate to the respective page to find the specifications that are relevant to you.

Info

All specifications have a 4-9 character long unique ID - a combination of letters and numbers. These letters only carry legacy meaning only leveraged by the AVM core team and are no longer used to group the specifications in any visible way. The ID is used to reference the specification in the code, documentation, and discussions.

Specification Tags

The following tags are used to qualify the specifications:

KeyAllowed ValuesMultiple/Single
LanguageBicep, TerraformMultiple
ClassResource, Pattern, UtilityMultiple
TypeFunctional, NonFunctionalSingle
CategoryTesting, Telemetry, Contribution/Support, Documentation, CodeStyle, Naming/Composition, Inputs/Outputs, Release/PublishingSingle
SeverityMUST, SHOULD, MAYSingle
PersonaOwner, ContributorMultiple
LifecycleInitial, BAU, EOLSingle
ValidationBicep: BCP/Manual, BCP/CI/Informational, BCP/CI/Enforced
Terraform: TF/Manual, TF/CI/Informational, TF/CI/Enforced
Single per language

Each tag is a concatenation of exactly one of the keys and one of the values, e.g., Language-Bicep, Class-Resource, Type-Functional, etc. When it’s marked as Multiple, it means that the tag can have multiple values, e.g., Language-Bicep, Language-Terraform, or Persona-Owner, Persona-Contributor, etc. When it’s marked as Single, it means that the tag can have only one value, e.g., Type-Functional, Lifecycle-Initial, etc.

βž• Click here to see the definition of the Severity, Persona, Lifecycle and Validation tags...

Severity

What’s the severity or importance of this specification? See “How to read the specifications?” section for more details.

Persona

Who is this specification for? The Owner is the module owner, while the Contributor is anyone who contributes to the module.

Lifecycle

When is this specification mostly relevant?

  • The Initial stage is when the module is being developed first - e.g., naming related specs are labeled with Lifecycle-Initial as the naming of the module only happens once: at the beginning of their life.
  • The BAU (business as usual) stage is at any time during the module’s typical lifecycle - e.g., specs that describe coding standards are relevant throughout the module’s life, for any time a new module version is released.
  • The EOL (end of life) stage is when the module is being decommissioned - e.g., specs describing how a module should be retired are labeled with Lifecycle-EOL.

Validation

How is this specification checked/validated/enforced?

  • Manual means that the specification is manually enforced at the time of the module review (at the time of the first or any subsequent module version release).
  • CI/Informational means that the module is checked against the specification by a CI pipeline, but the failure is only informational and doesn’t block the module release.
  • CI/Enforced means that the specification is automatically enforced by a CI pipeline, and the failure blocks the module release.

Note: the BCP/ or TF/ prefix is required as shared (language-agnostic) specifications may have different level of validation/enforcement per each language - e.g., it is possible that a specification is enforced by a CI pipeline for Bicep modules, while it is manually enforced for Terraform modules.

Why are there language specific specifications?

While every effort is being made to standardize requirements and implementation details across all languages (and most specifications in fact, are applicable to all), it is expected that some of the specifications will be different between their respective languages to ensure we follow the best practices and leverage features of each language.

How to read the specifications?

Important

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, β€œMAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

As you’re developing/maintaining a module as a module owner or contributor, you need to ensure that your module adheres to the specifications outlined in this section. The specifications are designed to ensure that all AVM modules are consistent, secure, and compliant with best practices.

There are 3 levels of specifications:

  • MUST: These are mandatory requirements that MUST be followed.
  • SHOULD: These are recommended requirements that SHOULD be followed, unless there are good reasons for not to.
  • MAY: These are optional requirements that MAY be followed at the module owner’s/contributor’s discretion.

Subsections of Module Specifications

Bicep Specifications

Specifications by Category and Module Classification

CategoryResourcePatternUtility
Contribution/Support988
Telemetry434
Naming/Composition25189
CodeStyle222
Inputs/Outputs141110
Testing141313
Documentation555
Release/Publishing555
Summary786556

How to propose changes to the specifications?

Important

Any updates to existing or new specifications for Bicep must be submitted as a draft for review by the AVM core team(@Azure/avm-core-team).

What changed recently?

See what specifications changed in the last 30 days...

#IDLast Modified (UTC)Git HistoryLast Commit
1BCPFR62026-03-20 11:06:37All Commits2f883f7
2BCPFR72026-03-20 11:06:37All Commits2f883f7
3BCPNFR242026-03-20 11:06:37All Commits2f883f7

Subsections of Bicep

Bicep Interfaces

This chapter details the interfaces/schemas for the AVM Resource Modules features/extension resources as referenced in RMFR4 and RMFR5.

Diagnostic Settings

Important

Allowed values for logs and metric categories or category groups MUST NOT be specified to keep the module implementation evergreen for any new categories or category groups added by RPs, without module owners having to update a list of allowed values and cut a new release of their module.

Diagnostic Settings
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service. If neither metrics nor logs are specified, all metrics & logs are configured by default. If only one of them is specified, the other one will not be configured.')
  param diagnosticSettings diagnosticSettingFullType[]?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (diagnosticSetting, index) in (diagnosticSettings ?? []): {
    name: diagnosticSetting.?name ?? '${name}-diagnosticSettings'
    properties: {
      storageAccountId: diagnosticSetting.?storageAccountResourceId
      workspaceId: diagnosticSetting.?workspaceResourceId
      eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId
      eventHubName: diagnosticSetting.?eventHubName
      metrics: [
        for group in (diagnosticSetting.?metricCategories ?? (empty(diagnosticSetting.?logCategoriesAndGroups)
          ? [{ category: 'AllMetrics' }]
          : [])): {
          category: group.category
          enabled: group.?enabled ?? true
          timeGrain: null
        }
      ]
      logs: [
        for group in (diagnosticSetting.?logCategoriesAndGroups ?? (empty(diagnosticSetting.?metricCategories)
          ? [{ categoryGroup: 'allLogs' }]
          : [])): {
          categoryGroup: group.?categoryGroup
          category: group.?category
          enabled: group.?enabled ?? true
        }
      ]
      marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
      logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
    }
    scope: >singularMainResourceType<
  }]
  
  diagnosticSettings: [
    {
      name: 'diagSetting1'
      logCategoriesAndGroups: [
        {
          category: 'AzurePolicyEvaluationDetails'
        }
        {
          category: 'AuditEvent'
        }
      ]
      metricCategories: [
        {
          category: 'AllMetrics'
        }
      ]
      logAnalyticsDestinationType: 'Dedicated'
      workspaceResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'
      storageAccountResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'
      eventHubAuthorizationRuleResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}'
      eventHubName: '{eventHubName}'
      marketplacePartnerResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}'
    }
  ]
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingMetricsOnlyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service.')
  param diagnosticSettings diagnosticSettingMetricsOnlyType[]?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (diagnosticSetting, index) in (diagnosticSettings ?? []): {
    name: diagnosticSetting.?name ?? '${name}-diagnosticSettings'
    properties: {
      storageAccountId: diagnosticSetting.?storageAccountResourceId
      workspaceId: diagnosticSetting.?workspaceResourceId
      eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId
      eventHubName: diagnosticSetting.?eventHubName
      metrics: [for group in (diagnosticSetting.?metricCategories ?? [ { category: 'AllMetrics' } ]): {
        category: group.category
        enabled: group.?enabled ?? true
        timeGrain: null
      }]
      marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
      logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
    }
    scope: >singularMainResourceType<
  }]
  
  diagnosticSettings: [
    {
      name: 'diagSetting1'
      metricCategories: [
        {
          category: 'AllMetrics'
        }
      ]
      logAnalyticsDestinationType: 'Dedicated'
      workspaceResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'
      storageAccountResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'
      eventHubAuthorizationRuleResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}'
      eventHubName: '{eventHubName}'
      marketplacePartnerResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}'
    }
  ]
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingLogsOnlyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service.')
  param diagnosticSettings diagnosticSettingLogsOnlyType[]?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (diagnosticSetting, index) in (diagnosticSettings ?? []): {
    name: diagnosticSetting.?name ?? '${name}-diagnosticSettings'
    properties: {
      storageAccountId: diagnosticSetting.?storageAccountResourceId
      workspaceId: diagnosticSetting.?workspaceResourceId
      eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId
      eventHubName: diagnosticSetting.?eventHubName
      logs: [for group in (diagnosticSetting.?logCategoriesAndGroups ?? [ { categoryGroup: 'allLogs' } ]): {
        categoryGroup: group.?categoryGroup
        category: group.?category
        enabled: group.?enabled ?? true
      }]
      marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
      logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
    }
    scope: >singularMainResourceType<
  }]
  
  diagnosticSettings: [
    {
      name: 'diagSetting1'
      logCategoriesAndGroups: [
        {
          category: 'AzurePolicyEvaluationDetails'
        }
        {
          category: 'AuditEvent'
        }
      ]
      logAnalyticsDestinationType: 'Dedicated'
      workspaceResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'
      storageAccountResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'
      eventHubAuthorizationRuleResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}'
      eventHubName: '{eventHubName}'
      marketplacePartnerResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}'
    }
  ]
  
Note

In the provided example for Diagnostic Settings, both logs and metrics are enabled for the associated resource. However, it is IMPORTANT to note that certain resources may not support both diagnostic setting types/categories. In such cases, the resource configuration MUST be modified accordingly to ensure proper functionality and compliance with system requirements.

Role Assignments

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. Array of role assignments to create.')
  param roleAssignments roleAssignmentType[]?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var builtInRoleNames = {
    // Add other relevant built-in roles here for your resource as per BCPNFR5
    Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
    Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')
    Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
    'Role Based Access Control Administrator (Preview)': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')
    'User Access Administrator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')
  }
  
  var formattedRoleAssignments = [
    for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, {
      roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains(roleAssignment.roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/')
            ? roleAssignment.roleDefinitionIdOrName
            : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName))
    })
  ]
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
    for (roleAssignment, index) in (formattedRoleAssignments ?? []): {
      name: roleAssignment.?name ?? guid(>singularMainResourceType<.id, roleAssignment.principalId, roleAssignment.roleDefinitionId)
      properties: {
        roleDefinitionId: roleAssignment.roleDefinitionId
        principalId: roleAssignment.principalId
        description: roleAssignment.?description
        principalType: roleAssignment.?principalType
        condition: roleAssignment.?condition
        conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set
        delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId
      }
      scope: >singularMainResourceType<
    }
  ]
  
  roleAssignments: [
    {
      roleDefinitionIdOrName: 'Owner'
      principalId: nestedDependencies.outputs.managedIdentityPrincipalId
      principalType: 'ServicePrincipal'
    }
    {
      roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c'
      principalId: nestedDependencies.outputs.managedIdentityPrincipalId
      principalType: 'ServicePrincipal'
    }
    {
      roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
      principalId: nestedDependencies.outputs.managedIdentityPrincipalId
      principalType: 'ServicePrincipal'
    }
    {
      name: guid('Custom role assignment name seed')
      roleDefinitionIdOrName: 'Storage Blob Data Reader'
      principalId: '00000000-0000-0000-0000-000000000000'
      principalType: 'Group'
      description: 'Group with read-only access'
      condition: '@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase 'foo_storage_container''
      conditionVersion: '2.0'
    }
  ]
  

Details on child, extension and cross-referenced resources:

  • Modules MUST support Role Assignments on child, extension and cross-referenced resources as well as the primary resource via parameters/variables

Resource Locks

  // ============== //
  //   Parameters   //
  // ============== //
  
  import { lockType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The lock settings of the service.')
  param lock lockType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_lock 'Microsoft.Authorization/locks@2020-05-01' = if (!empty(lock ?? {}) && lock.?kind != 'None') {
    name: lock.?name ?? 'lock-${name}'
    properties: {
      level: lock.?kind ?? ''
      notes: lock.?notes ?? (lock.?kind == 'CanNotDelete'
        ? 'Cannot delete resource or child resources.'
        : 'Cannot delete or modify the resource or child resources.')
    }
    scope: >singularMainResourceType<
  }
  
  lock: {
    kind: 'CanNotDelete'
    name: 'myCustomLockName'
    notes: 'This is a custom lock note.'
  }
  

Details on child and extension resources:

  • Locks SHOULD be able to be set for child resources of the primary resource in resource modules

Details on cross-referenced resources:

  • Locks MUST be automatically applied to cross-referenced resources if the primary resource has a lock applied.
    • This MUST also be able to be turned off for each of the cross-referenced resources by the module consumer via a parameter/variable if they desire

An example of this is a Key Vault module that has a Private Endpoints enabled. If a lock is applied to the Key Vault via the lock parameter/variable then the lock should also be applied to the Private Endpoint automatically, unless the privateEndpointLock/private_endpoint_lock (example name) parameter/variable is set to None

Tags

  @description('Optional. Tags of the resource.')
  param tags object?
  
  tags: {
    key: 'value'
    'another-key': 'another-value'
    integers: 123
  }
  

Details on child, extension and cross-referenced resources:

  • Tags MUST be automatically applied to child, extension and cross-referenced resources, if tags are applied to the primary resource.
    • By default, all tags set for the primary resource will automatically be passed down to child, extension and cross-referenced resources.
    • This MUST be able to be overridden by the module consumer so they can specify alternate tags for child, extension and cross-referenced resources, if they desire via a parameter/variable
      • If overridden by the module consumer, no merge/union of tags will take place from the primary resource and only the tags specified for the child, extension and cross-referenced resources will be applied

Managed Identities

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentityAllType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var formattedUserAssignedIdentities = reduce(map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), {}, (cur, next) => union(cur, next)) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  var identity = !empty(managedIdentities) ? {
    type: (managedIdentities.?systemAssigned ?? false) ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned') : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : null)
    userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
  } : null
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: name
    identity: identity
    properties: {
      ... // other properties
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  @description('The principal ID of the system assigned identity.')
  output systemAssignedMIPrincipalId string? = >singularMainResourceType<.?identity.?principalId
  
  managedIdentities: {
    systemAssigned: true
    userAssignedResourceIds: [
      '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'
      '/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}'
    ]
  }
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { managedIdentityOnlySysAssignedType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentityOnlySysAssignedType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var identity = !empty(managedIdentities)
    ? {
        type: (managedIdentities.?systemAssigned ?? false) ? 'SystemAssigned' : null
      }
    : null
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: name
    identity: identity
    properties: {
      ... // other properties
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  @description('The principal ID of the system assigned identity.')
  output systemAssignedMIPrincipalId string? = >singularMainResourceType<.?identity.?principalId
  
  managedIdentities: {
    systemAssigned: true
  }
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { managedIdentityOnlyUserAssignedType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentityOnlyUserAssignedType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var formattedUserAssignedIdentities = reduce(map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), {}, (cur, next) => union(cur, next)) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  var identity = !empty(managedIdentities)
    ? {
        type: !empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : 'None'
        userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
      }
    : null
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: name
    identity: identity
    properties: {
      ... // other properties
    }
  }
  
  managedIdentities: {
    userAssignedResourceIds: [
      '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'
      '/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}'
    ]
  }
  

Reason for differences in User Assigned data type in languages:

  • We do not foresee the Managed Identity Resource Provider team to ever add additional properties within the empty object ({}) value required on the input of a User Assigned Managed Identity.
  • In Bicep we therefore have removed the need for this to be declared and just converted it to a simple array of Resource IDs

Private Endpoints

Private Endpoints

E.g., for services that only have one private endpoint type.

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
  param privateEndpoints privateEndpointSingleServiceType[]?
  
  var enableReferencedModulesTelemetry = false // resource module
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module >singularMainResourceType<_privateEndpoints 'br/public:avm/res/network/private-endpoint:>version<' = [for (privateEndpoint, index) in (privateEndpoints ?? []): {
    name: '${uniqueString(deployment().name, location)}->singularMainResourceType<-PrivateEndpoint-${index}'
    scope: resourceGroup(
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2],
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4]
    )
    params: {
      // Variant 1: A default service can be assumed (i.e., for services that only have one private endpoint type)
      name: privateEndpoint.?name ?? 'pep-${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.?service ?? '>defaultServiceName<'}-${index}'
      privateLinkServiceConnections: privateEndpoint.?isManualConnection != true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.?service ?? '>defaultServiceName<'}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.?service ?? '>defaultServiceName<'
            ]
          }
        }
      ] : null
      manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.?service ?? '>defaultServiceName<'}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.?service ?? '>defaultServiceName<'
            ]
            requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.'
          }
        }
      ] : null
      subnetResourceId: privateEndpoint.subnetResourceId
      enableTelemetry: enableReferencedModulesTelemetry // resource module
      enableTelemetry: privateEndpoint.?enableTelemetry ?? enableTelemetry // pattern / utility module
      location: privateEndpoint.?location ?? reference(split(privateEndpoint.subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location
      lock: privateEndpoint.?lock ?? lock
      privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup
      roleAssignments: privateEndpoint.?roleAssignments
      tags: privateEndpoint.?tags ?? tags
      customDnsConfigs: privateEndpoint.?customDnsConfigs
      ipConfigurations: privateEndpoint.?ipConfigurations
      applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds
      customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName
    }
  }]
  
  @description('The private endpoints of the resource.')
  output privateEndpoints privateEndpointOutputType[] = [
    for (pe, index) in (privateEndpoints ?? []): {
      name: >singularMainResourceType<_privateEndpoints[index].outputs.name
      resourceId: >singularMainResourceType<_privateEndpoints[index].outputs.resourceId
      groupId: >singularMainResourceType<_privateEndpoints[index].outputs.?groupId!
      customDnsConfigs: >singularMainResourceType<_privateEndpoints[index].outputs.customDnsConfigs
      networkInterfaceResourceIds: >singularMainResourceType<_privateEndpoints[index].outputs.networkInterfaceResourceIds
    }
  ]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type privateEndpointOutputType = {
    @description('The name of the private endpoint.')
    name: string
  
    @description('The resource ID of the private endpoint.')
    resourceId: string
  
    @description('The group Id for the private endpoint Group.')
    groupId: string?
  
    @description('The custom DNS configurations of the private endpoint.')
    customDnsConfigs: {
      @description('FQDN that resolves to private endpoint IP address.')
      fqdn: string?
  
      @description('A list of private IP addresses of the private endpoint.')
      ipAddresses: string[]
    }[]
  
    @description('The IDs of the network interfaces associated with the private endpoint.')
    networkInterfaceResourceIds: string[]
  }
  
  privateEndpoints: {
    {
      name: 'myPeName'
      privateLinkServiceConnectionName: 'myPrivateLinkConnectionName'
      lock: 'CanNotDelete'
      tags: {
        'hidden-title': 'This is visible in the resource name'
      }
      subnetResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mysubnet'
      resourceGroupResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg'
      applicationSecurityGroupResourceIds: [
        '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/applicationSecurityGroups/myAsg'
      ]
      privateDnsZoneGroup: {
        privateDnsZoneGroupConfigs: [
          {
            name: 'config'
            privateDnsZoneResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/privateDnsZones/myZone'
          }
        ]
      }
      customDnsConfigs: [
        {
          fqdn: 'fqdn1.example.com'
          ipAddresses: [
            '10.0.0.1',
            '10.0.0.2'
          ]
        }
      ]
      networkInterfaceName: 'nic1'
      ipConfigurations: [
        {
          name: 'ipconfig1'
          groupId: 'vault'
          memberName: 'default'
          privateIpAddress: '10.0.0.7'
        }
      ]
      roleAssignments: [
        {
          roleDefinitionIdOrName: 'Owner'
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
        {
          roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions','acdd72a7-3385-48ef-bd42-f606fba81ae7')
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
      ]
    }
  }
  

E.g., for services that have more than one private endpoint type, like a Storage Account (blob, file, etc.)

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { privateEndpointMultiServiceType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
  param privateEndpoints privateEndpointMultiServiceType[]?
  
  var enableReferencedModulesTelemetry = false // resource module
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module >singularMainResourceType<_privateEndpoints 'br/public:avm/res/network/private-endpoint:>version<' = [for (privateEndpoint, index) in (privateEndpoints ?? []): {
    name: '${uniqueString(deployment().name, location)}->singularMainResourceType<-PrivateEndpoint-${index}'
    scope: resourceGroup(
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2],
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4]
    )
    params: {
      // Variant 2: A default service cannot be assumed (i.e., for services that have more than one private endpoint type, like Storage Account)
      name: privateEndpoint.?name ?? 'pep-${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.service}-${index}'
      privateLinkServiceConnections: privateEndpoint.?isManualConnection != true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.service}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.service
            ]
          }
        }
      ] : null
      manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.service}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.service
            ]
            requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.'
          }
        }
      ] : null
      subnetResourceId: privateEndpoint.subnetResourceId
      enableTelemetry: enableReferencedModulesTelemetry // resource module
      enableTelemetry: privateEndpoint.?enableTelemetry ?? enableTelemetry // pattern / utility module
      location: privateEndpoint.?location ?? reference(split(privateEndpoint.subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location
      lock: privateEndpoint.?lock ?? lock
      privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup
      roleAssignments: privateEndpoint.?roleAssignments
      tags: privateEndpoint.?tags ?? tags
      customDnsConfigs: privateEndpoint.?customDnsConfigs
      ipConfigurations: privateEndpoint.?ipConfigurations
      applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds
      customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName
    }
  }]
  
  @description('The private endpoints of the resource.')
  output privateEndpoints privateEndpointOutputType[] = [
    for (pe, index) in (privateEndpoints ?? []): {
      name: >singularMainResourceType<_privateEndpoints[index].outputs.name
      resourceId: >singularMainResourceType<_privateEndpoints[index].outputs.resourceId
      groupId: >singularMainResourceType<_privateEndpoints[index].outputs.?groupId!
      customDnsConfigs: >singularMainResourceType<_privateEndpoints[index].outputs.customDnsConfigs
      networkInterfaceResourceIds: >singularMainResourceType<_privateEndpoints[index].outputs.networkInterfaceResourceIds
    }
  ]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type privateEndpointOutputType = {
    @description('The name of the private endpoint.')
    name: string
  
    @description('The resource ID of the private endpoint.')
    resourceId: string
  
    @description('The group Id for the private endpoint Group.')
    groupId: string?
  
    @description('The custom DNS configurations of the private endpoint.')
    customDnsConfigs: {
      @description('FQDN that resolves to private endpoint IP address.')
      fqdn: string?
  
      @description('A list of private IP addresses of the private endpoint.')
      ipAddresses: string[]
    }[]
  
    @description('The IDs of the network interfaces associated with the private endpoint.')
    networkInterfaceResourceIds: string[]
  }
  
  privateEndpoints: {
    {
      name: 'myPeName'
      privateLinkServiceConnectionName: 'myPrivateLinkConnectionName'
      lock: 'CanNotDelete'
      tags: {
        'hidden-title': 'This is visible in the resource name'
      }
      service: 'blob'
      subnetResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mysubnet'
      resourceGroupResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg'
      applicationSecurityGroupResourceIds: [
        '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/applicationSecurityGroups/myAsg'
      ]
      privateDnsZoneGroup: {
        privateDnsZoneGroupConfigs: [
          {
            name: 'config'
            privateDnsZoneResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/privateDnsZones/myZone'
          }
        ]
      }
      customDnsConfigs: [
        {
          fqdn: 'fqdn1.example.com'
          ipAddresses: [
            '10.0.0.1',
            '10.0.0.2'
          ]
        }
      ]
      networkInterfaceName: 'nic1'
      ipConfigurations: [
        {
          name: 'ipconfig1'
          groupId: 'blob'
          memberName: 'default'
          privateIpAddress: '10.0.0.7'
        }
      ]
      roleAssignments: [
        {
          roleDefinitionIdOrName: 'Owner'
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
        {
          roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions','acdd72a7-3385-48ef-bd42-f606fba81ae7')
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
      ]
    }
  }
  

Notes:

  • The properties defined in the schema above are the minimum amount of properties expected to be exposed for Private Endpoints in AVM Resource Modules.
    • A module owner MAY chose to expose additional properties of the Private Endpoint resource
      • However, module owners considering this SHOULD contact the AVM core team first to consult on how the property should be exposed to avoid future breaking changes to the schema that may be enforced upon them
  • Module owners MAY chose to define a list of allowed value for the ‘service’ (a.k.a. groupIds) property
    • However, they should do so with caution as should a new service appear for their resource module, a new release will need to be cut to add this new service to the allowed values
      • Whereas not specifying allowed values will allow flexibility from day 0 without the need for any changes and releases to be made

Customer Managed Keys

Customer Managed Keys
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The customer managed key definition.')
  param customerManagedKey customerManagedKeyType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  // If user-assiged identities are supported => Adds any user assigned identity specified in the customer managed key definition to the general managed-identity spcification
  var formattedUserAssignedIdentities = reduce(
    map(
      union(
        (managedIdentities.?userAssignedResourceIds ?? []),
        (!empty(customerManagedKey.?userAssignedIdentityResourceId)
          ? [customerManagedKey.?userAssignedIdentityResourceId]
          : [])
      ),
      (id) => { '${id}': {} }
    ),
    {},
    (cur, next) => union(cur, next)
  ) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  
  var identity = !empty(managedIdentities) || !empty(formattedUserAssignedIdentities) 
    ? {
        type: (managedIdentities.?systemAssigned ?? false)
          ? (!empty(formattedUserAssignedIdentities) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned')
          : (!empty(formattedUserAssignedIdentities) ? 'UserAssigned' : null)
        userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
      }
    : null
  
  var isHSMManagedCMK = split(customerManagedKey.?keyVaultResourceId ?? '', '/')[?7] == 'managedHSMs'
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource cMKKeyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
    name: last(split((customerManagedKey!.?keyVaultResourceId!), '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?keyVaultResourceId!, '/')[2],
      split(customerManagedKey!.?keyVaultResourceId!, '/')[4]
    )
  
    resource cMKKey 'keys@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
      name: customerManagedKey!.?keyName!
    }
  }
  
  resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) {
    name: last(split(customerManagedKey!.?userAssignedIdentityResourceId!, '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[2],
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[4]
    )
  }
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      encryption: !empty(customerManagedKey)
        ? {
            keySource: 'Microsoft.KeyVault'
            keyVaultProperties: {
              keyVaultUri: !isHSMManagedCMK
                  ? cMKKeyVault!.properties.vaultUri
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/'
              keyName: customerManagedKey!.keyName
              keyVersion: !empty(customerManagedKey!.?keyVersion)
                ? customerManagedKey!.keyVersion!
                : (!isHSMManagedCMK
                  ? last(split(cMKKeyVault::cMKKey!.properties.keyUriWithVersion, '/'))
                  : fail('Managed HSM CMK encryption requires specifying the \'keyVersion\'.'))
              keyIdentifier: !empty(customerManagedKey!.?keyVersion)
                ? ( !isHSMManagedCMK
                  ? '${cMKKeyVault::cMKKey!.properties.keyUri}/${customerManagedKey!.keyVersion!}'
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}/${customerManagedKey!.keyVersion!}')
                : ( !isHSMManagedCMK
                  ? cMKKeyVault::cMKKey!.properties.keyUriWithVersion
                  : fail('Managed HSM CMK encryption requires specifying the \'keyVersion\'.'))
              identityClientId: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? cMKUserAssignedIdentity!.properties.clientId
                : null
              identity: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? {
                    userAssignedIdentity: cMKUserAssignedIdentity!.id
                  }
                : null
            }
          }
        : null
    }
  }
  
  customerManagedKey: {
    keyVaultResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}'
    keyName: '{keyName}'
    keyVersion: '{keyVersion}'
    userAssignedIdentityResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}'
  }
  
  // ============== //
  //   Parameters   //
  // ============== //
  import { customerManagedKeyWithAutoRotateType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The customer managed key definition.')
  param customerManagedKey customerManagedKeyWithAutoRotateType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  // If user-assiged identities are supported => Adds any user assigned identity specified in the customer managed key definition to the general managed-identity spcification
  var formattedUserAssignedIdentities = reduce(
    map(
      union(
        (managedIdentities.?userAssignedResourceIds ?? []),
        (!empty(customerManagedKey.?userAssignedIdentityResourceId)
          ? [customerManagedKey.?userAssignedIdentityResourceId]
          : [])
      ),
      (id) => { '${id}': {} }
    ),
    {},
    (cur, next) => union(cur, next)
  ) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  
  var identity = !empty(managedIdentities) || !empty(formattedUserAssignedIdentities) 
    ? {
        type: (managedIdentities.?systemAssigned ?? false)
          ? (!empty(formattedUserAssignedIdentities) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned')
          : (!empty(formattedUserAssignedIdentities) ? 'UserAssigned' : null)
        userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
      }
    : null
    
  var isHSMManagedCMK = split(customerManagedKey.?keyVaultResourceId ?? '', '/')[?7] == 'managedHSMs'
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource cMKKeyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
    name: last(split((customerManagedKey!.?keyVaultResourceId!), '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?keyVaultResourceId!, '/')[2],
      split(customerManagedKey!.?keyVaultResourceId!, '/')[4]
    )
  
    resource cMKKey 'keys@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
      name: customerManagedKey!.?keyName!
    }
  }
  
  resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) {
    name: last(split(customerManagedKey!.?userAssignedIdentityResourceId!, '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[2],
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[4]
    )
  }
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      encryption: !empty(customerManagedKey)
        ? {
            keySource: 'Microsoft.KeyVault'
            keyVaultProperties: {
              keyVaultUri: !isHSMManagedCMK
                  ? cMKKeyVault!.properties.vaultUri
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/'
              keyName: customerManagedKey!.keyName
              keyVersion: !empty(customerManagedKey!.?keyVersion)
                  ? customerManagedKey!.keyVersion!
                  : (customerManagedKey!.?autoRotationEnabled ?? true)
                      ? null
                      : (!isHSMManagedCMK
                          ? last(split(cMKKeyVault::cMKKey!.properties.keyUriWithVersion, '/'))
                          : fail('Managed HSM CMK encryption requires either specifying the \'keyVersion\' or omitting the \'autoRotationEnabled\' property. Setting \'autoRotationEnabled\' to false without a \'keyVersion\' is not allowed.'))
              keyIdentifier: !empty(customerManagedKey!.?keyVersion)
                ? (!isHSMManagedCMK
                  ? '${cMKKeyVault::cMKKey!.properties.keyUri}/${customerManagedKey!.keyVersion!}'
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}/${customerManagedKey!.keyVersion!}')
                : (customerManagedKey!.?autoRotationEnabled ?? true)
                  ? (!isHSMManagedCMK
                    ? cMKKeyVault::cMKKey!.properties.keyUri
                    : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}')
                  : (!isHSMManagedCMK
                    ? cMKKeyVault::cMKKey!.properties.keyUriWithVersion
                    : fail('Managed HSM CMK encryption requires either specifying the \'keyVersion\' or omitting the \'autoRotationEnabled\' property. Setting \'autoRotationEnabled\' to false without a \'keyVersion\' is not allowed.'))
              identityClientId: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? cMKUserAssignedIdentity!.properties.clientId
                : null
              identity: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? {
                    userAssignedIdentity: cMKUserAssignedIdentity!.id
                  }
                : null
            }
          }
        : null
    }
  }
  
  customerManagedKey: {
    keyVaultResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}'
    keyName: '{keyName}'
    autoRotationEnabled: {true|false}
    userAssignedIdentityResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}'
  }
  

Secrets export (DEPRECATED)

Important

Since version Bicep 0.35.1, it is possible to export secrets securely using the secure() annotation.

As this approach is fairly simple compared with the below workaround it is highly recommended to use it instead.

Example

@secure()
@description('The primary connection string of the service bus namespace.')
output primaryConnectionString string = listkeys(
  '${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey',
  '2024-01-01'
).primaryConnectionString

@secure()
@description('The primary key of the service bus namespace.')
output primaryKey string = listkeys(
  '${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey',
  '2024-01-01'
).primaryKey

Secrets used inside a module can be exported to a Key Vault reference provided as per the below schema.
This implementation provides a secure way around the current limitation of Bicep on providing a secure template output (that can be used for secrets).

The user MUST

  • provide the resource Id to a Key Vault. The principal used for the deployment MUST be allowed to set secrets in this Key Vault.
  • provide a name for each secret they want to store (opt-in). The module will suggest which secrets are available via the implemented user-defined type.

The module returns an output table where the key is the name of the secret the user provided, and the value contains both the secret’s resource Id and URI.

Important

The feature MUST be implemented as per the below schema. Diversions are only allowed in places marked as >text< to ensure a consistent user experience across modules.

User Defined Type, Parameter & Resource Example

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
  param secretsExportConfiguration secretsExportConfigurationType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
    name: '${uniqueString(deployment().name, location)}-secrets-kv'
    scope: resourceGroup(
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[2],
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[4]
    )
    params: {
      keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId, '/'))
      secretsToSet: union(
        [],
        contains(secretsExportConfiguration!, '>secretToExport1<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport1<Name
                value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
              }
            ]
          : [],
        contains(secretsExportConfiguration!, '>secretToExport2<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport2<Name
                value:>secretReference2<  // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
              }
            ]
          : []
          // (...)
      )
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
  output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
    ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
    : {}
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type secretsExportConfigurationType = {
    @description('Required. The resource ID of the key vault where to store the secrets of this module.')
    keyVaultResourceId: string
  
    @description('Optional. The >secretToExport1< secret name to create.')
    >secretToExport1<Name: string?
  
    @description('Optional. The >secretToExport2< secret name to create.')
    >secretToExport2<Name: string?
  
    // (...)
  }
  

Input Example with Values

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
  param secretsExportConfiguration secretsExportConfigurationType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
    name: '${uniqueString(deployment().name, location)}-secrets-kv'
    scope: resourceGroup(
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[2],
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[4]
    )
    params: {
      keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId, '/'))
      secretsToSet: union(
        [],
        contains(secretsExportConfiguration!, '>secretToExport1<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport1<Name
                value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
              }
            ]
          : [],
        contains(secretsExportConfiguration!, '>secretToExport2<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport2<Name
                value:>secretReference2<  // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
              }
            ]
          : []
          // (...)
      )
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
  output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
    ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
    : {}
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type secretsExportConfigurationType = {
    @description('Required. The resource ID of the key vault where to store the secrets of this module.')
    keyVaultResourceId: string
  
    @description('Optional. The >secretToExport1< secret name to create.')
    >secretToExport1<Name: string?
  
    @description('Optional. The >secretToExport2< secret name to create.')
    >secretToExport2<Name: string?
  
    // (...)
  }
  

[modules/keyVaultExport.bicep] file

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Required. The name of the Key Vault to set the secrets in.')
  param keyVaultName string
  
  import { secretToSetType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Required. The secrets to set in the Key Vault.')
  param secretsToSet secretToSetType[]
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
    name: keyVaultName
  }
  
  resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [
    for secret in secretsToSet: {
      name: secret.name
      parent: keyVault
      properties: {
        value: secret.value
      }
    }
  ]
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretSetOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('The references to the secrets exported to the provided Key Vault.')
  output secretsSet secretSetOutputType[] = [
    #disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value
    for index in range(0, length(secretsToSet ?? [])): {
      secretResourceId: secrets[index].id
      secretUri: secrets[index].properties.secretUri
      secretUriWithVersion: secrets[index].properties.secretUriWithVersion
    }
  ]
  

Output Usage Example

When using a module that implements the above interface, you can access its outputs for example in the following ways:

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
  param secretsExportConfiguration secretsExportConfigurationType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
    name: '${uniqueString(deployment().name, location)}-secrets-kv'
    scope: resourceGroup(
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[2],
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[4]
    )
    params: {
      keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId, '/'))
      secretsToSet: union(
        [],
        contains(secretsExportConfiguration!, '>secretToExport1<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport1<Name
                value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
              }
            ]
          : [],
        contains(secretsExportConfiguration!, '>secretToExport2<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport2<Name
                value:>secretReference2<  // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
              }
            ]
          : []
          // (...)
      )
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
  output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
    ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
    : {}
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type secretsExportConfigurationType = {
    @description('Required. The resource ID of the key vault where to store the secrets of this module.')
    keyVaultResourceId: string
  
    @description('Optional. The >secretToExport1< secret name to create.')
    >secretToExport1<Name: string?
  
    @description('Optional. The >secretToExport2< secret name to create.')
    >secretToExport2<Name: string?
  
    // (...)
  }
  

Which returns a JSON-formatted output like:

  {
    "exportedSecrets": {
      "Type": "Object",
      "Value": {
        ">secretToExportName1<": {
          "secretResourceId": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<",
          "secretUri": "https://<vaultName>.vault.azure.net/secrets/>secretToExportName1<"
        },
        ">secretToExportName2<": {
          "secretResourceId": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName2<",
          "secretUri": "https://<vaultName>.vault.azure.net/secrets/>secretToExportName2<"
        }
      }
    },
    "specificSecret": {
      "Type": "String",
      "Value": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<"
    },
    "exportedSecretResourceIds": {
      "Type": "Array",
      "Value": [
        "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<",
        "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName2<"
      ]
    }
  }
  

Azure Monitor Alerts

Note

This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.

Zonal & zone-redundant resources

Many Azure resources can be deployed into specific availability zones. Depending on whether a resource is ‘zonal’ (i.e., deploys a single instance into a single zone) or ‘zone-redundant’ (i.e., spreads multiple of its instances across the configured zones), implementing a different interface is required. Simply put, the zone of a zonal resource must be a required parameter (but give the user the option to ‘opt-out’), while zone-redundant resources must span all available zones by default, but still give the user the option to ‘opt-out’. Please note that the support for Availability Zones may differ from region to region.

  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Required. If set to 1, 2 or 3, the availability zone is hardcoded to that value. If set to -1, no zone is defined. Note that the availability zone numbers here are the logical availability zone in your Azure subscription. Different subscriptions might have a different mapping of the physical zone and logical zone. To understand more, please refer to [Physical and logical availability zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview?tabs=azure-cli#physical-and-logical-availability-zones).')
  @allowed([
    -1
    1
    2
    3
  ])
  param availabilityZone int
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      zones: availabilityZone != -1 ? array(string(availabilityZone)) : null // If expecting an array
      // Or
      availabilityZone: availabilityZone != -1 ? string(availabilityZone) : null // If expecting a single value
    }
  }
  
  availabilityZone: -1 // Deploy into no zone
  availabilityZone: 1 // Deploy into zone 1
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. The list of Availability zones to use for the zone-redundant resources.')
  @allowed([
    1
    2
    3
  ])
  param availabilityZones int[] = [1, 2, 3]
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      zones: map(availabilityZones, zone => '${zone}')
    }
  }
  
  availabilityZones: [] // Deploy into no zone
  availabilityZones: [1, 2] // Deploy into zone 1 & 2
  

Bicep Pattern Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8BCPNFR15AVM Module Issue template fileMUSTOwnerBAU
βž• See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

ID: SNFR11 - Category: Contribution/Support - Issues Response Times

A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.




See origin...

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.




See origin...

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...



Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
3BCPFR4Telemetry EnablementMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}



See origin...

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.




See origin...

ID: BCPFR4 - Category: Composition - Telemetry Enablement

To comply with specifications outlined in SFR3 & SFR4 you MUST incorporate the following code snippet into your modules. Place this code sample in the “top level” main.bicep file; it is not necessary to include it in any nested Bicep files (child modules), unless they are marked for direct publishing (Ref Child module publishing).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: []
      outputs: {
        telemetry: {
          type: 'String'
          value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
        }
      }
    }
  }
}



Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6PMFR1Resource Group CreationMAYOwnerContributorBAU
7PMNFR1Module NamingMUSTOwnerInitial
8PMNFR2Use Resource Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
9PMNFR3Use other Pattern Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
10BCPFR1Cross-Referencing ModulesMAYOwnerContributorBAU
11BCPFR2Role Assignments Role Definition MappingMUSTOwnerContributorBAU
12BCPFR6Cross-Referencing Child-ModulesMUSTOwnerContributorBAU
13BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
14BCPNFR23Module compositionMUSTOwnerContributorBAU
15BCPNFR24Deterministic Deployment NamesMUSTOwnerContributorBAU
16BCPNFR5Role Assignments Role Definition Mapping LimitsSHOULDOwnerContributorBAU
17BCPNFR6Role Assignments Role Definition Mapping Compulsory RolesMUSTOwnerContributorBAU
18BCPNFR14VersioningMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”



See origin...

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.




See origin...

ID: PMFR1 - Category: Composition - Resource Group Creation

A Pattern Module MAY create Resource Group(s).




See origin...

ID: PMNFR1 - Category: Naming - Module Naming

Pattern Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Pattern Module Naming

  • Naming convention: avm/ptn/<hyphenated grouping/category name>/<hyphenated pattern module name>
  • Example: avm/ptn/compute/app-tier-vmss or avm/ptn/avd-lza/management-plane or avm/ptn/3-tier/web-app
  • Segments:
    • ptn defines this as a pattern module
    • <hyphenated grouping/category name> is a hierarchical grouping of pattern modules by category, with each word separated by dashes, such as:
      • project name, e.g., avd-lza,
      • primary resource provider, e.g., compute or network, or
      • architecture, e.g., 3-tier
    • <hyphenated pattern module name> is a term describing the module’s function, with each word separated by dashes, e.g., app-tier-vmss = Application Tier VMSS; management-plane = Azure Virtual Desktop Landing Zone Accelerator Management Plane

Terraform Pattern Module Naming

  • Naming convention:
    • avm-ptn-<pattern module name> (Module name for registry)
    • terraform-<provider>-avm-ptn-<pattern module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-ptn-apptiervmss or avm-ptn-avd-lza-managementplane
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • ptn defines this as a pattern module
    • <pattern module name> is a term describing the module’s function, e.g., apptiervmss = Application Tier VMSS; avd-lza-managementplane = Azure Virtual Desktop Landing Zone Accelerator Management Plane



See origin...

ID: PMNFR2 - Category: Composition - Use Resource Modules to Build a Pattern Module

A Pattern Module SHOULD be built from AVM Resources Modules to establish a standardized code base and improve maintainability. If a valid reason exists, a pattern module MAY contain native resources (“vanilla” code) where it’s necessary. A Pattern Module MUST NOT contain references to non-AVM modules.

Valid reasons for not using a Resource Module for a resource required by a Pattern Module include but are not limited to:

  • When using a Resource Module would result in hitting scaling limitations and/or would reduce the capabilities of the Pattern Module due to the limitations of Azure Resource Manager.
  • Developing a Pattern Module under time constraint, without having all required Resource Modules readily available.
Note

In the latter case, the Pattern Module SHOULD be updated to use the Resource Module when the required Resource Module becomes available, to avoid accumulating technical debt. Ideally, all required Resource Modules SHOULD be developed first, and then leveraged by the Pattern Module.




See origin...

ID: PMNFR3 - Category: Composition - Use other Pattern Modules to Build a Pattern Module

A Pattern Module MAY contain and be built using other AVM Pattern Modules. A Pattern Module MUST NOT contain references to non-AVM modules.




See origin...

ID: BCPFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-reference other modules to build either Resource or Pattern modules.

However, they MUST be referenced only by a public registry reference to a pinned version e.g. br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<. They MUST NOT use local parent path references to a module e.g. ../../xxx/yyy.bicep.

The only exception to this rule are child modules as documented in BCPFR6.

Modules MUST NOT contain references to non-AVM modules.




See origin...

ID: BCPFR2 - Category: Composition - Role Assignments Role Definition Mapping

Module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID, this should be self contained within the module themselves.

However, they MUST use only the official RBAC Role Definition name within the variable and nothing else.

To meet the requirements of BCPFR2, BCPNFR5 and BCPNFR6 you MUST use the below code sample in your AVM Modules to achieve this.

  @description('''Required. You can provide either the display name (note not all roles are supported, check module documentation) of the role definition, or its fully qualified ID in the following format: `/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11`.''')
  param roleDefinitionIdOrName string
  
  var builtInRbacRoleNames = {
    Owner: '/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
    Contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    Reader: '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
    'Role Based Access Control Administrator (Preview)': '/providers/Microsoft.Authorization/roleDefinitions/f58310d9-a9f6-439a-9e8d-f62e7b41a168'
    'User Access Administrator': '/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
    //Other RBAC Role Definitions Names & IDs can be added here as needed for your module
  }
  
  var roleDefinitionIdMappedResult = (contains(builtInRbacRoleNames, roleDefinitionIdOrName) ? builtInRbacRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName)
  
  resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
    //Other properties removed for ease of reading
    properties: {
      roleDefinitionId: roleDefinitionIdMappedResult
      //Other properties removed for ease of reading
    }
  }
  



See origin...

ID: BCPFR6 - Cross-Referencing Child-Modules

Parent templates MUST reference all their direct child-templates to allow for an end-to-end deployment experience.
For example, the SQL server template must reference its child database module and encapsulate it in a loop to allow for the deployment of multiple databases.

@description('Optional. The databases to create in the server')
param databases databaseType[]?

resource server 'Microsoft.Sql/servers@(...)' = { (...) }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct



See origin...

ID: BCPNFR23 - Category: Composition

Each Bicep AVM module that lives within the Azure/bicep-registry-modules (BRM) repository in the avm directory MUST have the following directories and files:

  • /tests - (for unit tests and additional E2E/integration if required - e.g. Pester etc.)
    • /e2e - (all examples must deploy successfully - these will be used to automatically generate the examples in the README.md for the module)
  • /src - (for scripts and other files - e.g., scripts used by the template)
    • exampleFile.ps1
  • /modules - (for sub-modules only if used and NOT children of the primary resource - e.g. RBAC role assignments)
    • exampleTemplate.bicep
  • /main.bicep (AVM Module main .bicep file and entry point/orchestration module)
  • /main.json (auto generated and what is published to the MCR via BRM)
  • /version.json (BRM requirement)
  • /README.md (auto generated AVM Module documentation)
  • /CHANGELOG.md (manually maintained changelog file with one entry per published version)

Directory and File Structure Example

/ Root of Azure/bicep-registry-modules
β”‚
β”œβ”€β”€β”€avm
β”‚   β”œβ”€β”€β”€ptn
β”‚   β”‚   └───apptiervmss
β”‚   β”‚       β”‚   main.bicep
β”‚   β”‚       β”‚   main.json
β”‚   β”‚       β”‚   README.md
β”‚   β”‚       β”‚   CHANGELOG.md
β”‚   β”‚       β”‚   version.json
β”‚   β”‚       β”œβ”€β”€β”€src (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€Get-Cake.ps1
β”‚   β”‚       β”‚   └───Find-Waldo.ps1
β”‚   β”‚       β”œβ”€β”€β”€modules (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€helper.bicep
β”‚   β”‚       β”‚   └───role-assignment.bicep
β”‚   β”‚       └───tests
β”‚   β”‚           β”œβ”€β”€β”€unit (optional)
β”‚   β”‚           └───e2e
β”‚   β”‚               β”œβ”€β”€β”€defaults
β”‚   β”‚               β”œβ”€β”€β”€waf-aligned
β”‚   β”‚               └───max
β”‚   β”‚
β”‚   └───res
β”‚       └───compute
β”‚           └───virtual-machine
β”‚               β”‚   main.bicep
β”‚               β”‚   main.json
β”‚               β”‚   README.md
β”‚               β”‚   CHANGELOG.md
β”‚               β”‚   version.json
β”‚               β”œβ”€β”€β”€src (optional)
β”‚               β”‚   β”œβ”€β”€β”€Set-Bug.ps1
β”‚               β”‚   └───Invoke-Promotion.ps1
β”‚               β”œβ”€β”€β”€modules (optional)
β”‚               β”‚   β”œβ”€β”€β”€helper.bicep
β”‚               β”‚   └───role-assignment.bicep
β”‚               └───tests
β”‚                   β”œβ”€β”€β”€unit (optional)
β”‚                   └───e2e
β”‚                       β”œβ”€β”€β”€defaults
β”‚                       β”œβ”€β”€β”€waf-aligned
β”‚                       └───max
β”œβ”€β”€β”€other repo dirs...
└───other repo files...



See origin...

ID: BCPNFR24 - Category: Naming/Composition - Deterministic Deployment Names

When a module references child, utility, or other modules, the deployment name MUST be deterministic. This means the deployment name must produce the same value for the same set of inputs across repeated deployments.

Why deterministic?

Azure Resource Manager has an 800-deployment limit per scope (resource group, subscription, management group, tenant). Non-deterministic names (e.g., those incorporating timestamps or utcNow()) create a new deployment object on every run, which can lead to this limit being reached over time.

While an automatic cleanup process exists for resource group and subscription scopes, it can take some time to take effect. Due to eventual consistency in the backend, the deployment count may not reflect the cleanup immediately, which can lead to failed deployments even when the actual number of deployments is below the 800 limit. Additionally, automatic cleanup does not apply to management group or tenant scopes.

We are actively working with the product team to enhance the cleanup process. In the meantime, deterministic deployment names provide a reliable way to keep deployment counts stable by overwriting previous deployment objects rather than creating new ones.

Deterministic deployment names cause Azure to overwrite the previous deployment object, keeping the deployment count stable regardless of how many times the module is deployed.

Requirement

Module owners MUST construct deployment names for referenced modules using uniqueString() seeded with the parent resource’s ID (<parentResource>.id) and location, rather than deployment().name, subscription().id, resourceGroup().id, utcNow(), or other non-deterministic or scope-level values.

The deployment name MUST follow the pattern:

'${uniqueString(<parentResource>.id, location)}-<ChildModuleDescriptor>-${index}'

Where:

SegmentDescription
uniqueString(<parentResource>.id, location)A deterministic hash derived from the parent resource’s resource ID and deployment location. This is both unique per resource instance and stable across deployments.
<ChildModuleDescriptor>A short, human-readable label identifying the child module being deployed (e.g., DB, Subnet, FederatedIdentityCred).
${index}The loop index variable, included when deploying in a loop. Omit for single (non-looped) deployments.
location parameter

If location is not available, for example when deploying a global resource that does not have a location property, it is acceptable to omit it. However, the <parentResource>.id MUST always be included as the primary seed for uniqueString.

Why parent resource ID?

Using the parent resource’s ID as the uniqueString seed provides two critical properties:

  1. Deterministic β€” the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. Collision-free β€” different parent resource instances produce different hashes, so deploying multiple instances of the same module type within the same scope does not cause naming collisions.
Why not subscription().id and resourceGroup().id separately?

The parent resource’s ID (e.g., /subscriptions/.../resourceGroups/.../providers/.../resourceName) already contains the subscription ID and resource group ID as segments. Using <parentResource>.id as a single input to uniqueString captures all of this context in one value, keeping the code concise and readable rather than passing multiple scope-level values separately.

Supporting multiple deployments of the same module at the same scope

A common scenario is deploying the same module type more than once within the same scope β€” for example, two different SQL servers each with their own set of databases, or two user-assigned identities each with their own federated credentials. Because the parent resource ID is unique per resource instance, the resulting deployment names will differ even when the child module type and index are identical. This ensures that parallel deployments of the same module at the same scope do not collide.

Other approaches fail on one or both of these properties:

ApproachDeterministic?Collision-free?Issue
deployment().nameβŒβœ…Changes every deployment; hits 800-limit
utcNow() / timestampsβŒβœ…Changes every deployment; hits 800-limit
subscription().id + resourceGroup().idβœ…βŒSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationβœ…βœ…Recommended β€” stable and unique per instance

Examples

Example 1: Single child module deployment

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_database 'database/main.bicep' = {
  name: '${uniqueString(server.id, location)}-Sql-DB'
  params: {
    serverName: server.name
    (...)
  }
}

Example 2: Child module deployment in a loop

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

ID: BCPNFR5 - Category: Composition - Role Assignments Role Definition Mapping Limits

As per BCPFR2, module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID.

Module owners SHOULD NOT map every RBAC Role Definition within this variable as it can cause the module to bloat in size and cause consumption issues later when stitched together with other modules due to the 4MB ARM Template size limit.

Therefore module owners SHOULD only map the most applicable and common RBAC Role Definition names for their module and SHOULD NOT exceed 15 RBAC Role Definitions in the variable.

Important

Remember if the RBAC Role Definition name is not included in the variable this does not mean it cannot be declared, used and assigned to an identity via an RBAC Role Assignment as part of a module, as any RBAC Role Definition can be specified via its ID without being in the variable.

Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

ID: BCPNFR6 - Category: Composition - Role Assignments Role Definition Mapping Compulsory Roles

Module owners MUST include the following roles in the variable for RBAC Role Definition names:

  • Owner - ID: 8e3af657-a8ff-443c-a75c-2fe8c4bcb635
  • Contributor - ID: b24988ac-6180-42a0-ab88-20f7382dd24c
  • Reader - ID: acdd72a7-3385-48ef-bd42-f606fba81ae7
  • User Access Administrator - ID: 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9
  • Role Based Access Control Administrator (Preview) - ID: f58310d9-a9f6-439a-9e8d-f62e7b41a168
Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

ID: BCPNFR14 - Category: Composition - Versioning

To meet SNFR17 and depending on the changes you make, you may need to bump the version in the version.json file.

  {
    "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
    "version": "0.1"
  }
  

The version value is in the form of MAJOR.MINOR. The PATCH version will be incremented by the CI automatically when publishing the module to the Public Bicep Registry once the corresponding pull request is merged. Therefore, contributions that would only require an update of the patch version, can keep the version.json file intact.

For example, the version value should be:

  • 0.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4PMNFR5Parameter/Variable NamingSHOULDOwnerContributorBAU
5BCPNFR1Complex data types - GeneralMUSTOwnerContributorBAU
6BCPNFR9Inputs - DecoratorsMUSTOwnerContributorBAU
7BCPNFR18User-defined types - SpecificationMUSTOwnerContributorBAU
8BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
9BCPNFR20User-defined types - ExportMUSTOwnerContributorBAU
10BCPNFR21User-defined types - DecoratorsMUSTOwnerContributorBAU
11BCPNFR7Parameter Requirement TypesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR14 - Category: Inputs - Data Types

A module SHOULD use either: simple data types. e.g., string, int, bool.

OR

Complex data types (objects, arrays, maps) when the language-compliant schema is defined.




See origin...

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: PMNFR5 - Category: Inputs - Parameter/Variable Naming

Parameter/variable input names SHOULD contain the resource to which they pertain. E.g., virtualMachineSku/virtualmachine_sku




See origin...

ID: BCPNFR1 - Category: Inputs - Complex data types - General

To simplify the consumption experience for module consumers when interacting with complex data types input parameters, mainly objects and arrays, the Bicep features of Resource-Derived Types or User-Defined Types MUST be used and declared.

Tip

User-Defined Types are GA in Bicep as of version v0.21.1, Resource-Derived Types are GA as of version v0.34.1, please ensure you have this version(s) installed as a minimum.

Resource-Derived Types and User-Defined Types allow intellisense support in supported IDEs (e.g. Visual Studio Code) for complex input parameters using objects and array of objects.

v0.x Exemption

While we allow the release of major versions, starting with v1.0.0, retrofitting Resource-Derived Types and User-Defined Types for all modules will take a considerable amount of time.

Therefore, the addition of these features is currently NOT mandated/enforced. However, all modules MUST implement Resource-Derived Types and User-Defined Types prior to the release of their v1.0.0 version.




See origin...

ID: BCPNFR9 - Inputs - Decorators

Similar to BCPNFR21, input parameters MUST implement decorators such as description & secure (if sensitive).

Further, input parameters SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
param threshold: int?
@description('Required. The SKU of your resource.')
@allowed([
'Basic'
'Premium'
'Standard'
])
param sku string



See origin...

ID: BCPNFR18 - User-defined types - Specification

User-defined types (UDTs) MUST always be singular and non-nullable. The configuration of either should instead be done directly at the parameter or output that uses the type.

For example, instead of

param subnets subnetsType
type subnetsType = { ... }[]?

the type should be defined like

param subnets subnetType[]?
type subnetType = { ... }

The primary reason for this requirement is clarity. If not defined directly at the parameter or output, a user would always be required to check the type to understand how e.g., a parameter is expected.




See origin...

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct



See origin...

ID: BCPNFR20 - User-defined types - Export

User-defined types (UDTs) SHOULD always be exported via the @export() annotation in every template they’re implemented in.

@export()
type subnetType = { ... }

Doing so has the benefit that other (e.g., parent) modules can import them and as such reduce code duplication. Also, if the module itself is published, users of the Public Bicep Registry can import the types independently of the module itself. One example where this can be useful is a pattern module that may re-use the same interface when referencing a module from the registry.




See origin...

ID: BCPNFR21 - User-defined types - Decorators

Similar to BCPNFR9, User-defined types (UDTs) MUST implement decorators such as description & secure (if sensitive). This is true for every property of the UDT, as well as the UDT itself.

Further, User-defined types SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('My type''s description.')
type myType = {
  @description('Optional. The threshold of your resource.')
  @minValue(1)
  @maxValue(10)
  threshold: int?

  @description('Required. The SKU of your resource.')
  sku: ('Basic' | 'Premium' | 'Standard')
}



See origin...

ID: BCPNFR7 - Category: Inputs - Parameter Requirement Types

Modules will have lots of parameters that will differ in their requirement type (required, optional, etc.). To help consumers understand what each parameter’s requirement type is, module owners MUST add the requirement type to the beginning of each parameter’s description. Below are the requirement types with a definition and example for the description decorator:

Parameter Requirement TypeDefinitionExample Description Decorator
RequiredThe parameter value must be provided. The parameter does not have a default value and hence the module expects and requires an input.@description('Required. <PARAMETER DESCRIPTION HERE...>')
ConditionalThe parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. Should contain a sentence starting with ‘Required if (…).’ to explain the condition.@description('Conditional. <PARAMETER DESCRIPTION HERE...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe parameter value is generated within the module and should not be specified as input in most cases. A common example of this is the utcNow() function that is only supported as the input for a parameter value, and not inside a variable.@description('Generated. <PARAMETER DESCRIPTION HERE...>')



Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9BCPNFR10Test Bicep File NamingMUSTOwnerContributorBAU
10BCPNFR11Test ToolingMUSTOwnerContributorBAU
11BCPNFR12Deployment Test NamingMUSTOwnerContributorBAU
12BCPNFR13Test file metadataMUSTOwnerContributorBAU
13BCPNFR16Post-deployment testsMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).




See origin...

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.




See origin...

ID: SNFR5 - Category: Testing - Upgrade Tests

Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.




See origin...

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.




See origin...

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

ID: BCPNFR10 - Category: Testing - Test Bicep File Naming

Module owners MUST name their test .bicep files in the /tests/e2e/<defaults/waf-aligned/max/etc.> directories: main.test.bicep as the test framework (CI) relies upon this name.




See origin...

ID: BCPNFR11 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.

  • PSRule for Azure
  • Pester
    • Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Pester for your own tests.



See origin...

ID: BCPNFR12 - Category: Testing - Deployment Test Naming

Module owners MUST invoke the module in their test using the syntax:

module testDeployment '../../../main.bicep' =

Example 1: Working example with a single deployment

module testDeployment '../../../main.bicep' = {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}'
  params: {
    (...)
  }
}

Example 2: Working example using a deployment loop

@batchSize(1)
module testDeployment '../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}'
  params: {
    (...)
  }
}]

The syntax is used by the ReadMe-generating utility to identify, pull & format usage examples.




See origin...

ID: BCPNFR13 - Category: Testing - Test file metadata

By default, the ReadMe-generating utility will create usage examples headers based on each e2e folder’s name.
Module owners MAY provide a custom name & description by specifying the metadata blocks name & description in their main.test.bicep test files.

For example:

metadata name = 'Using Customer-Managed-Keys with System-Assigned identity'
metadata description = 'This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.'

would lead to a header in the module’s readme.md file along the lines of

### Example 1: _Using Customer-Managed-Keys with System-Assigned identity_

This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.



See origin...

ID: BCPNFR16 - Category: Testing - Post-deployment tests

For each test case in the e2e folder, you can optionally add post-deployment Pester tests that are executed once the corresponding deployment completed and before the removal logic kicks in.

To leverage the feature you MUST:

  • Use Pester as a test framework in each test file

  • Name the file with the suffix "*.tests.ps1"

  • Place each test file the e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file returns, as well as the path to the test template file in case you want to extract data from it directly.

    For example, with an output such as output resourceId string = testDeployment[1].outputs.resourceId defined in the main.test.bicep file, the $TestInputData would look like:

    $TestInputData = @{
      DeploymentOutputs    = @{
        resourceId = @{
          Type  = "String"
          Value = "/subscriptions/***/resourceGroups/dep-***-keyvault.vaults-kvvpe-rg/providers/Microsoft.KeyVault/vaults/***kvvpe001"
        }
      }
      ModuleTestFolderPath = "/home/runner/work/bicep-registry-modules/bicep-registry-modules/avm/res/key-vault/vault/tests/e2e/private-endpoint"
    }

    A full test file may look like:

    βž• Pester post-deployment test file example
    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )
    
    Describe 'Validate private endpoint deployment' {
    
        Context 'Validate sucessful deployment' {
    
            It "Private endpoints should be deployed in resource group" {
    
                $keyVaultResourceId = $TestInputData.DeploymentOutputs.resourceId.Value
                $testResourceGroup = ($keyVaultResourceId -split '\/')[4]
                $deployedPrivateEndpoints = Get-AzPrivateEndpoint -ResourceGroupName $testResourceGroup
                $deployedPrivateEndpoints.Count | Should -BeGreaterThan 0
            }
        }
    }



Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3BCPNFR2Module Documentation GenerationMUSTOwnerContributorBAU
4BCPNFR3Usage Example formatsMUSTOwnerContributorBAU
5BCPNFR4Parameter Input ExamplesMAYOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR15 - Category: Documentation - Automatic Documentation Generation

README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: BCPNFR2 - Category: Documentation - Module Documentation Generation

Note

This script/tool is currently being developed by the AVM team and will be made available very soon.

Bicep modules documentation MUST be automatically generated via the provided script/tooling from the AVM team, providing the following headings:

  • Title
  • Description
  • Navigation
  • Resource Types
  • Usage Examples
  • Parameters
  • Outputs
  • Cross-referenced modules



See origin...

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <resourceName> 'br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<' = {
      name: '${uniqueString(deployment().name, location)}-test-<uniqueIdentifier>'
      params: { (...) }
    }
  • JSON / ARM Template Parameter Files - .json

    {
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
      "contentVersion": "1.0.0.0",
      "parameters": { (...) }
    }
Note

The above formats are currently automatically taken & generated from the tests/e2e tests. It is enough to run the Set-ModuleReadMe or Set-AVMModule functions (from the utilities folder) to update the usage examples in the readme(s).

Note

Bicep Parameter Files (.bicepparam) are being reviewed and considered by the AVM team for the usability and features at this time and will likely be added in the future.




See origin...

ID: BCPNFR4 - Category: Documentation - Parameter Input Examples

Bicep modules MAY provide parameter input examples for parameters using the metadata.example property via the @metadata() decorator.

Example:

@metadata({
  example: 'uksouth'
})
@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@metadata({
  example: '''
  {
    keyName: 'myKey'
    keyVaultResourceId: '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/myvault'
    keyVersion: '6d143c1a0a6a453daffec4001e357de0'
    userAssignedIdentityResourceId '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity'
  }
  '''
})
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyType

It is planned that these examples are automatically added to the module readme’s parameter descriptions when running either the Set-ModuleReadMe or Set-AVMModule scripts (available in the utilities folder).




Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
5BCPNFR22Bicep Module ChangelogMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.




See origin...

ID: BCPNFR22 - Category: Publishing - Changelog

When a module to be published (i.e., that has a version.json file) is changed, an entry MUST be created in the CHANGELOG.md file in the module folder. A link to the latest version of the changelog file has to be included at the top of the file, just below the # Changelog line. It is surrounded by empty lines.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/<ptn|res|utl>/<namespace/modulename[/submodulePath]>/CHANGELOG.md).

For each new version, an entry MUST be created above all existing versions in the CHANGELOG.md file of the module.

## <version>

### Changes

- This changed
- And this also

### Breaking Changes

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may be added as content for a section.
  • MUST exist only once.
  • All versions appear in descending order, which puts the most recent changes at the top.

What SHOULD be listed in the (Breaking) Changes section:

  • Relevant changes for the module
  • Changes in tests do not need to be added
Note

The versioning is following the SNFR17 - Semantic Versioning spec.

Example content of the CHANGELOG.md

A CHANGELOG.md file in the module’s root folder MUST start with the # Changelog header, followed by an empty line and a link to the latest published version of the changelog file, followed by another empty line. A section for each published version follows. Newer versions are placed above older versions.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/res/aad/domain-service/CHANGELOG.md).

## 0.2.1

### Changes

- Updated the referenced AVM common types

### Breaking Changes

- None

## 0.2.0

### Changes

- Implemented the minCPU parameter
- Updated the referenced VirtualNetwork module
- Updated the referenced AVM common types

### Breaking Changes

- The minCPU parameter is mandatory

## 0.1.0

### Changes

- Initial Release

### Breaking Changes

- None

Each bullet point should start with a capital letter.

Manual Editing

It is possible to modify the changelog content any time, e.g., to add missing versions, which will not create a new release of the module itself. Please note the following requirements in all cases:

  • All versions in the file, need to be valid and available as published version
  • Every version needs the two sections ## Changes and ## Breaking Changes with content
Note

Azure Verified Modules are artifacts in the Microsoft Container Registry (MCR). Every version of a module exists as a tag in the Container Registry and can be listed as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1BCPNFR8Code Styling - lower camelCasingSHOULDOwnerContributorBAU
2BCPNFR17Code Styling - Type castingSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: BCPNFR8 - Category: Composition - Code Styling - lower camelCasing

Module owners SHOULD use lower camelCasing for naming the following:

  • Parameters
  • Variables
  • Outputs
  • User Defined Types
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: camelCasingExample (lowercase first word (entirely), with capital of first letter of all other words and rest of word in lowercase)




See origin...

ID: BCPNFR17 - Category: Composition - Code Styling - Type casting

To improve the usability of primitive module properties declared as strings, you SHOULD declare them using a type which better represents them, and apply any required casting in the module on behalf of the user.

For reference, please refer to the following examples:

Boolean as String

Boolean as String
@allowed([
  'false'
  'true'
])
param myParameterValue string = 'false'

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: myParameterValue
  }
}
param myParameterValue string = false

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: string(myParameterValue)
  }
}

Integer Array as String Array

Integer Array as String Array
@allowed([
  '1'
  '2'
  '3'
])
param zones array

resource myResource '(...)' = {
  (...)
  properties: {
    zones: zones
  }
}
@allowed([
  1
  2
  3
])
param zones int[]

resource myResource '(...)' = {
  (...)
  properties: {
    zones: map(zones, zone => string(zone))
  }
}



Bicep Resource Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8PMNFR4Missing Resource Module(s)MUSTOwnerContributorBAU
9BCPNFR15AVM Module Issue template fileMUSTOwnerBAU
βž• See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

ID: SNFR11 - Category: Contribution/Support - Issues Response Times

A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.




See origin...

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.




See origin...

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: PMNFR4 - Category: Hygiene - Missing Resource Module(s)

An item MUST be logged onto as an issue on the AVM Central Repo (Azure/Azure-Verified-Modules) if a Resource Module does not exist for resources deployed by the pattern module.

Exception

If the Resource Module adds no value, see Resource Module functional requirement ID: RMFR2.




See origin...

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...



Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
3BCPFR4Telemetry EnablementMUSTOwnerContributorBAU
4BCPFR7Cross-Referencing ModulesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}



See origin...

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.




See origin...

ID: BCPFR4 - Category: Composition - Telemetry Enablement

To comply with specifications outlined in SFR3 & SFR4 you MUST incorporate the following code snippet into your modules. Place this code sample in the “top level” main.bicep file; it is not necessary to include it in any nested Bicep files (child modules), unless they are marked for direct publishing (Ref Child module publishing).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: []
      outputs: {
        telemetry: {
          type: 'String'
          value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
        }
      }
    }
  }
}



See origin...

ID: BCPFR7 - Cross-Referencing published Modules

Resource modules, that reference other modules (child, utility, or other resource modules), MUST disable the telemetry on the referenced modules.

Note

This only applies to resource modules that reference other modules, such as:

  • other resource modules
  • utility modules
  • child-modules qualifying for publishing, i.e. having a version.json file in their directory and exposing the enableTelemetry input parameter

For pattern modules, SFR4 still applies.

A variable named enableReferencedModulesTelemetry is created in the main.bicep file of the module, that cross-references other published modules, and set to false. This variable is used to set the enableTelemetry parameter of cross-referenced modules.

var enableReferencedModulesTelemetry = false

// local referencing
module virtualNetwork_subnets 'subnet/main.bicep' = [
  for (subnet, index) in (subnets ?? []): {
    name: '${uniqueString(virtualNetwork.id, location)}-subnet-${index}'
    params: {
      (...)
      enableTelemetry: enableReferencedModulesTelemetry
    }
  }
]

// published module reference
module virtualNetwork_subnet 'br/public:avm/res/network/virtual-network/subnet:0.1.0' = {
  name: '${uniqueString(virtualNetwork.id, location)}-subnet-${index}'
    params: {
      (...)
      enableTelemetry: enableReferencedModulesTelemetry
    }
}



Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6RMFR1Single Resource OnlyMUSTOwnerContributorBAU
7RMFR2No Resource Wrapper ModulesMUSTOwnerInitial
8RMFR3Resource GroupsMUSTOwnerContributorBAU
9RMFR4AVM Consistent Feature & Extension Resources Value AddMUSTOwnerContributorBAU
10RMFR5AVM Consistent Feature & Extension Resources Value Add Interfaces/SchemasMUSTOwnerContributorBAU
11RMFR8Dependency on child and other resourcesMUSTOwnerContributorBAU
12RMFR9End-of-life resource versionsSHOULDOwnerContributorBAU
13RMNFR1Module NamingMUSTOwnerInitial
14RMNFR3RP CollaborationSHOULDOwnerBAU
15BCPFR1Cross-Referencing ModulesMAYOwnerContributorBAU
16BCPFR2Role Assignments Role Definition MappingMUSTOwnerContributorBAU
17BCPFR6Cross-Referencing Child-ModulesMUSTOwnerContributorBAU
18BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
19BCPNFR23Module compositionMUSTOwnerContributorBAU
20BCPNFR24Deterministic Deployment NamesMUSTOwnerContributorBAU
21BCPNFR5Role Assignments Role Definition Mapping LimitsSHOULDOwnerContributorBAU
22BCPNFR6Role Assignments Role Definition Mapping Compulsory RolesMUSTOwnerContributorBAU
23BCPNFR14VersioningMUSTOwnerContributorBAU
24BCPRMNFR3Child resources structureMUSTOwnerContributorBAU
25BCPRMNFR4Multi-scope modulesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”



See origin...

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.




See origin...

ID: RMFR1 - Category: Composition - Single Resource Only

A resource module MUST only deploy a single instance of the primary resource, e.g., one virtual machine per instance.

Multiple instances of the module MUST be used to scale out.




See origin...

ID: RMFR2 - Category: Composition - No Resource Wrapper Modules

A resource module MUST add value by including additional features on top of the primary resource.




See origin...

ID: RMFR3 - Category: Composition - Resource Groups

A resource module MUST NOT create a Resource Group for resources that require them.

In the case that a Resource Group is required, a module MUST have an input (scope or variable):

  • In Bicep the targetScope MUST be set to resourceGroup or not specified (which means default to resourceGroup scope)
  • In Terraform the variable MUST be called resource_group_name

Scopes will be covered further in the respective language specific specifications.




See origin...

ID: RMFR4 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add

Resource modules support the following optional features/extension resources, as specified, if supported by the primary resource. The top-level variable/parameter names MUST be:

Optional Features/Extension ResourcesBicep Parameter NameTerraform Variable NameMUST/SHOULD
Diagnostic SettingsdiagnosticSettingsdiagnostic_settingsMUST
Role AssignmentsroleAssignmentsrole_assignmentsMUST
Resource LockslocklockMUST
TagstagstagsMUST
Managed Identities (System / User Assigned)managedIdentitiesmanaged_identitiesMUST
Private EndpointsprivateEndpointsprivate_endpointsMUST
Customer Managed KeyscustomerManagedKeycustomer_managed_keyMUST
Azure Monitor AlertsalertsalertsSHOULD

Resource modules MUST NOT deploy required/dependent resources for the optional features/extension resources specified above. For example, for Diagnostic Settings the resource module MUST NOT deploy the Log Analytics Workspace, this is expected to be already in existence from the perspective of the resource module deployed via another method/module etc.

Note

Please note that the implementation of Customer Managed Keys from an ARM API perspective is different across various RPs that implement Customer Managed Keys in their service. For that reason you may see differences between modules on how Customer Managed Keys are handled and implemented, but functionality will be as expected.

Module owners MAY choose to utilize cross repo dependencies for these “add-on” resources, or MAY chose to implement the code directly in their own repo/module. So long as the implementation and outputs are as per the specifications requirements, then this is acceptable.

Tip

Make sure to checkout the language specific specifications for more info on this:




See origin...

ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas

Resource modules MUST implement a common interface, e.g. the input’s data structures and properties within them (objects/arrays/dictionaries/maps), for the optional features/extension resources:

See:




See origin...

ID: RMFR8 - Category: Composition - Dependency on child and other resources

A resource module MAY contain references to other resource modules, however MUST NOT contain references to non-AVM modules nor AVM pattern modules.

See BCPFR1 and TFFR1 for more information on this.




See origin...

ID: RMFR9 - Category: Composition - End-of-life resource versions

When a given version of an Azure resource used in a resource module reaches its end-of-life (EOL) and is no longer supported by Microsoft, the module owner SHOULD ensure that:

  1. The module is aligned with these changes and only includes supported versions of the resource. This is typically achieved through the allowed values in the parameter that specifies the resource SKU or type.
  2. The following notice is shown under the Notes section of the module’s readme.md. (If any related public announcement is available, it can also be linked to from the Notes section.):

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from the related parameters.”

  3. AND the related parameter’s description:

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from this parameter.”




See origin...

ID: RMNFR1 - Category: Naming - Module Naming

Resource modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Note

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the correct singular names for all resource types to enable checks to utilize this list to ensure repos are named correctly. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

This will be updated quarterly, or ad-hoc as new RPs/ Resources are created and highlighted via a check failure.

Bicep Resource Module Naming

  • Naming convention (module name for registry): avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>
  • Example: avm/res/compute/virtual-machine or avm/res/managed-identity/user-assigned-identity
  • Segments:
    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute = compute, Microsoft.ManagedIdentity = managed-identity.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute/virtualMachines = virtual-machine, BUT Microsoft.Network/trafficmanagerprofiles = trafficmanagerprofile - since trafficmanagerprofiles is all lower case as per the ARM API definition.

Bicep Child Module Naming

  • Naming convention (module name for registry):avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>/ <hyphenated child resource type/<hyphenated grandchild resource type>/<etc.>

  • Example: avm/res/network/virtual-network/subnet or avm/res/storage/storage-account/blob-service/container

  • Segments:

    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network = network.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks = virtual-network.
    • <hyphenated child resource type (to be repeated for grandchildren, etc.)> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks/subnets = subnet or Microsoft.Storage/storageAccounts/blobServices/containers = blob-service/container.

Terraform Resource Module Naming

  • Naming convention:
    • avm-res-<resource provider>-<ARM resource type> (module name for registry)
    • terraform-<provider>-avm-res-<resource provider>-<ARM resource type> (GitHub repository name to meet registry naming requirements)
  • Example: avm-res-compute-virtualmachine or avm-res-managedidentity-userassignedidentity
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • res defines this is a resource module
    • <resource provider> is the resource provider’s name after the Microsoft part, e.g., Microsoft.Compute = compute.
    • <ARM resource type> is the singular version of the word after the resource provider, e.g., Microsoft.Compute/virtualMachines = virtualmachine



See origin...

ID: RMNFR3 - Category: Composition - RP Collaboration

Module owners (Microsoft FTEs) SHOULD reach out to the respective Resource Provider teams to build a partnership and collaboration on the modules creation, existence and long term maintenance.

Review this wiki page (Microsoft Internal) for more information.




See origin...

ID: BCPFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-reference other modules to build either Resource or Pattern modules.

However, they MUST be referenced only by a public registry reference to a pinned version e.g. br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<. They MUST NOT use local parent path references to a module e.g. ../../xxx/yyy.bicep.

The only exception to this rule are child modules as documented in BCPFR6.

Modules MUST NOT contain references to non-AVM modules.




See origin...

ID: BCPFR2 - Category: Composition - Role Assignments Role Definition Mapping

Module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID, this should be self contained within the module themselves.

However, they MUST use only the official RBAC Role Definition name within the variable and nothing else.

To meet the requirements of BCPFR2, BCPNFR5 and BCPNFR6 you MUST use the below code sample in your AVM Modules to achieve this.

  @description('''Required. You can provide either the display name (note not all roles are supported, check module documentation) of the role definition, or its fully qualified ID in the following format: `/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11`.''')
  param roleDefinitionIdOrName string
  
  var builtInRbacRoleNames = {
    Owner: '/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
    Contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    Reader: '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
    'Role Based Access Control Administrator (Preview)': '/providers/Microsoft.Authorization/roleDefinitions/f58310d9-a9f6-439a-9e8d-f62e7b41a168'
    'User Access Administrator': '/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
    //Other RBAC Role Definitions Names & IDs can be added here as needed for your module
  }
  
  var roleDefinitionIdMappedResult = (contains(builtInRbacRoleNames, roleDefinitionIdOrName) ? builtInRbacRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName)
  
  resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
    //Other properties removed for ease of reading
    properties: {
      roleDefinitionId: roleDefinitionIdMappedResult
      //Other properties removed for ease of reading
    }
  }
  



See origin...

ID: BCPFR6 - Cross-Referencing Child-Modules

Parent templates MUST reference all their direct child-templates to allow for an end-to-end deployment experience.
For example, the SQL server template must reference its child database module and encapsulate it in a loop to allow for the deployment of multiple databases.

@description('Optional. The databases to create in the server')
param databases databaseType[]?

resource server 'Microsoft.Sql/servers@(...)' = { (...) }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct



See origin...

ID: BCPNFR23 - Category: Composition

Each Bicep AVM module that lives within the Azure/bicep-registry-modules (BRM) repository in the avm directory MUST have the following directories and files:

  • /tests - (for unit tests and additional E2E/integration if required - e.g. Pester etc.)
    • /e2e - (all examples must deploy successfully - these will be used to automatically generate the examples in the README.md for the module)
  • /src - (for scripts and other files - e.g., scripts used by the template)
    • exampleFile.ps1
  • /modules - (for sub-modules only if used and NOT children of the primary resource - e.g. RBAC role assignments)
    • exampleTemplate.bicep
  • /main.bicep (AVM Module main .bicep file and entry point/orchestration module)
  • /main.json (auto generated and what is published to the MCR via BRM)
  • /version.json (BRM requirement)
  • /README.md (auto generated AVM Module documentation)
  • /CHANGELOG.md (manually maintained changelog file with one entry per published version)

Directory and File Structure Example

/ Root of Azure/bicep-registry-modules
β”‚
β”œβ”€β”€β”€avm
β”‚   β”œβ”€β”€β”€ptn
β”‚   β”‚   └───apptiervmss
β”‚   β”‚       β”‚   main.bicep
β”‚   β”‚       β”‚   main.json
β”‚   β”‚       β”‚   README.md
β”‚   β”‚       β”‚   CHANGELOG.md
β”‚   β”‚       β”‚   version.json
β”‚   β”‚       β”œβ”€β”€β”€src (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€Get-Cake.ps1
β”‚   β”‚       β”‚   └───Find-Waldo.ps1
β”‚   β”‚       β”œβ”€β”€β”€modules (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€helper.bicep
β”‚   β”‚       β”‚   └───role-assignment.bicep
β”‚   β”‚       └───tests
β”‚   β”‚           β”œβ”€β”€β”€unit (optional)
β”‚   β”‚           └───e2e
β”‚   β”‚               β”œβ”€β”€β”€defaults
β”‚   β”‚               β”œβ”€β”€β”€waf-aligned
β”‚   β”‚               └───max
β”‚   β”‚
β”‚   └───res
β”‚       └───compute
β”‚           └───virtual-machine
β”‚               β”‚   main.bicep
β”‚               β”‚   main.json
β”‚               β”‚   README.md
β”‚               β”‚   CHANGELOG.md
β”‚               β”‚   version.json
β”‚               β”œβ”€β”€β”€src (optional)
β”‚               β”‚   β”œβ”€β”€β”€Set-Bug.ps1
β”‚               β”‚   └───Invoke-Promotion.ps1
β”‚               β”œβ”€β”€β”€modules (optional)
β”‚               β”‚   β”œβ”€β”€β”€helper.bicep
β”‚               β”‚   └───role-assignment.bicep
β”‚               └───tests
β”‚                   β”œβ”€β”€β”€unit (optional)
β”‚                   └───e2e
β”‚                       β”œβ”€β”€β”€defaults
β”‚                       β”œβ”€β”€β”€waf-aligned
β”‚                       └───max
β”œβ”€β”€β”€other repo dirs...
└───other repo files...



See origin...

ID: BCPNFR24 - Category: Naming/Composition - Deterministic Deployment Names

When a module references child, utility, or other modules, the deployment name MUST be deterministic. This means the deployment name must produce the same value for the same set of inputs across repeated deployments.

Why deterministic?

Azure Resource Manager has an 800-deployment limit per scope (resource group, subscription, management group, tenant). Non-deterministic names (e.g., those incorporating timestamps or utcNow()) create a new deployment object on every run, which can lead to this limit being reached over time.

While an automatic cleanup process exists for resource group and subscription scopes, it can take some time to take effect. Due to eventual consistency in the backend, the deployment count may not reflect the cleanup immediately, which can lead to failed deployments even when the actual number of deployments is below the 800 limit. Additionally, automatic cleanup does not apply to management group or tenant scopes.

We are actively working with the product team to enhance the cleanup process. In the meantime, deterministic deployment names provide a reliable way to keep deployment counts stable by overwriting previous deployment objects rather than creating new ones.

Deterministic deployment names cause Azure to overwrite the previous deployment object, keeping the deployment count stable regardless of how many times the module is deployed.

Requirement

Module owners MUST construct deployment names for referenced modules using uniqueString() seeded with the parent resource’s ID (<parentResource>.id) and location, rather than deployment().name, subscription().id, resourceGroup().id, utcNow(), or other non-deterministic or scope-level values.

The deployment name MUST follow the pattern:

'${uniqueString(<parentResource>.id, location)}-<ChildModuleDescriptor>-${index}'

Where:

SegmentDescription
uniqueString(<parentResource>.id, location)A deterministic hash derived from the parent resource’s resource ID and deployment location. This is both unique per resource instance and stable across deployments.
<ChildModuleDescriptor>A short, human-readable label identifying the child module being deployed (e.g., DB, Subnet, FederatedIdentityCred).
${index}The loop index variable, included when deploying in a loop. Omit for single (non-looped) deployments.
location parameter

If location is not available, for example when deploying a global resource that does not have a location property, it is acceptable to omit it. However, the <parentResource>.id MUST always be included as the primary seed for uniqueString.

Why parent resource ID?

Using the parent resource’s ID as the uniqueString seed provides two critical properties:

  1. Deterministic β€” the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. Collision-free β€” different parent resource instances produce different hashes, so deploying multiple instances of the same module type within the same scope does not cause naming collisions.
Why not subscription().id and resourceGroup().id separately?

The parent resource’s ID (e.g., /subscriptions/.../resourceGroups/.../providers/.../resourceName) already contains the subscription ID and resource group ID as segments. Using <parentResource>.id as a single input to uniqueString captures all of this context in one value, keeping the code concise and readable rather than passing multiple scope-level values separately.

Supporting multiple deployments of the same module at the same scope

A common scenario is deploying the same module type more than once within the same scope β€” for example, two different SQL servers each with their own set of databases, or two user-assigned identities each with their own federated credentials. Because the parent resource ID is unique per resource instance, the resulting deployment names will differ even when the child module type and index are identical. This ensures that parallel deployments of the same module at the same scope do not collide.

Other approaches fail on one or both of these properties:

ApproachDeterministic?Collision-free?Issue
deployment().nameβŒβœ…Changes every deployment; hits 800-limit
utcNow() / timestampsβŒβœ…Changes every deployment; hits 800-limit
subscription().id + resourceGroup().idβœ…βŒSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationβœ…βœ…Recommended β€” stable and unique per instance

Examples

Example 1: Single child module deployment

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_database 'database/main.bicep' = {
  name: '${uniqueString(server.id, location)}-Sql-DB'
  params: {
    serverName: server.name
    (...)
  }
}

Example 2: Child module deployment in a loop

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

ID: BCPNFR5 - Category: Composition - Role Assignments Role Definition Mapping Limits

As per BCPFR2, module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID.

Module owners SHOULD NOT map every RBAC Role Definition within this variable as it can cause the module to bloat in size and cause consumption issues later when stitched together with other modules due to the 4MB ARM Template size limit.

Therefore module owners SHOULD only map the most applicable and common RBAC Role Definition names for their module and SHOULD NOT exceed 15 RBAC Role Definitions in the variable.

Important

Remember if the RBAC Role Definition name is not included in the variable this does not mean it cannot be declared, used and assigned to an identity via an RBAC Role Assignment as part of a module, as any RBAC Role Definition can be specified via its ID without being in the variable.

Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

ID: BCPNFR6 - Category: Composition - Role Assignments Role Definition Mapping Compulsory Roles

Module owners MUST include the following roles in the variable for RBAC Role Definition names:

  • Owner - ID: 8e3af657-a8ff-443c-a75c-2fe8c4bcb635
  • Contributor - ID: b24988ac-6180-42a0-ab88-20f7382dd24c
  • Reader - ID: acdd72a7-3385-48ef-bd42-f606fba81ae7
  • User Access Administrator - ID: 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9
  • Role Based Access Control Administrator (Preview) - ID: f58310d9-a9f6-439a-9e8d-f62e7b41a168
Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

ID: BCPNFR14 - Category: Composition - Versioning

To meet SNFR17 and depending on the changes you make, you may need to bump the version in the version.json file.

  {
    "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
    "version": "0.1"
  }
  

The version value is in the form of MAJOR.MINOR. The PATCH version will be incremented by the CI automatically when publishing the module to the Public Bicep Registry once the corresponding pull request is merged. Therefore, contributions that would only require an update of the patch version, can keep the version.json file intact.

For example, the version value should be:

  • 0.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.



See origin...

ID: BCPRMNFR3 - Implementing child resources

Child resource modules MUST be stored in a subfolder of their parent resource module and named after the child resource’s singular name (ref), so that the path to the child resource folder is consistent with the hierarchy of its resource type.
For example, Microsoft.Sql/servers may have dedicated child resources of type Microsoft.Sql/servers/databases. Hence, the SQL server database child module is stored in a database subfolder of the server parent folder.

sql
└─ server [module]
  └─ database [child-module/resource]

In this folder, we recommend to place the child resource-template alongside a ReadMe & compiled JSON (to be generated via the default Set-AVMModule utility) and optionally further nest additional folders for its child resources.

There are several reasons to structure a module in this way. For example:

  • It allows a separation of concerns where each module can focus on its own properties and logic, while delegating most of a child-resource’s logic to its separate child module
  • It’s consistent with the provider namespace structure and makes modules easier to understand not only because they’re more aligned with set structure, but also are aligned with one another
  • As each module is its own ‘deployment’, it reduces limitations around nested loops
  • It enables module owners to publish child-modules as separate modules to the public registry, allowing consumers to make use of them directly [Ref child module publishing guidelines for details].
Note

In full transparency: The drawbacks of these additional deployments is an extended deployment period & a contribution to the 800 deployments limit. However, for AVM resource modules it was agreed that the advantages listed above outweigh these limitations.




See origin...

ID: BCPRMNFR4 - Implementing multi-scope modules

Several resource types in Azure (e.g., role-assignments, budgets, etc.) may be deployed to more than one scope (e.g., subscription, management-group, etc.).
In AVM, such modules can be implemented in one of two ways:

  1. As pattern modules with one ‘orchestrating’ parent module using scoped sub-modules based on the input parameters provided

Note: Only the parent module is published. I.e., it is not possible to target e.g., the resource-group scoped sub-module directly.

  1. As resource modules where each scope is implemented as a child-module of a non-published parent.

Note: Each child module is published, but not the parent. I.e., it is possible to target e.g., the resource-group scoped sub-module directly.

Tip

It is highly recommended to publish multi-scoped modules as resource modules as the solution provides the best user experience.

Considerations when published as a pattern module

Example: avm/ptn/authorization/role-assignment

Note

The following instructions consider all deployment scopes. Your module may only deploy to a subset of the same and you should map the conventions to your case.

To successfully implement a multi-scoped module as a pattern modules you have to adhere to the following convention:

  • The parent module MUST be implemented in the highest scope the resource provider supports (e.g., management-group)
  • The parent module MUST have one sub-module for each scope that the resource provider supports (e.g., management-group, subscription & resource-group)
  • Each sub-module MUST be implemented for the scope it is intended
  • The parent module MUST invoke each sub-module in the scope it is written for, using input parameters needed to target set scope (e.g., a subscription-id to invoke a module for set scope)
  • The parent module MUST have test cases to validate each sub-module
  • The parent module is the one that is versioned, published and maintains a changelog

The full folder structure may look like

πŸ“„main.bicep                 [Orchestrating module]
πŸ“„main.json                  [ARM JSON file of the module]
πŸ“„version.json               [Version file of the module]
πŸ“„README.md                  [Readme of the module]
πŸ“„CHANGELOG.md               [The changelog of the module]
┣ πŸ“‚modules
┃ ┣ πŸ“„management-group.bicep [Sub-module deploying to the mgmt-group scope (if applicable)]
┃ ┣ πŸ“„subscription.bicep     [Sub-module deploying to the subscription scope (if applicable)]
┃ β”— πŸ“„resource-group.bicep   [Sub-module deploying to the resource-group scope (if applicable)]
β”— πŸ“‚tests/e2e
  ┣ πŸ“‚ mg.defaults
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template]
  ┣ πŸ“‚ mg.waf-aligned
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template]
  ┣ πŸ“‚ sub.defaults
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` param]
  ┣ πŸ“‚ sub.waf-aligned
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` param]
  ┣ πŸ“‚ rg.defaults
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` & `resourceGroupName` params]
  β”— πŸ“‚ rg.waf-aligned
    β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` & `resourceGroupName` params]
Warning

Even if a consumer wants to deploy to one of the sub-scopes (e.g., subscription), the module must be deployed via its parent (e.g., management-group). This can be confusing for consumers at first and should be considered when implementing the solution.

Example: To use a role-assignment pattern module (which would be written for all scopes, with the parent targeting the management-group scope) to deploy role assignments to a resource group, a user would need to invoke New-AzManagementGroupDeployment and provide the parameters for both the subscription & resource-group to target. I.e., the user must have permissions to deploy to each scope.

Considerations when published as a resource module

Example: avm/res/authorization/role-assignment

Note

The following instructions consider all deployment scopes. Your module may only deploy to a subset of the same and you should map the conventions to your case.

To successfully implement a multi-scoped module as a resource modules you have to adhere to the following convention:

  • The parent folder MUST contain a

    • main.bicep file
    • main.json file
    • README.md file
    • tests/e2e folder
    • One folder per each scope the resource provider can deploy to (either mg-scope, sub-scope or rg-scope).
  • Each child-module folder MUST be implemented as a proper child module, with a

    • main.bicep
    • main.json
    • version.json
    • README.md
    • CHANGELOG.md

    file. Each child-module is maintained and versioned independently of the others.

  • The parent main.bicep MUST contain the following information

    metadata name = '<Module Name> (Multi-Scope)'
    metadata description = '''
    This module's child-modules deploy a <Placeholder> at a Management Group (mg-scope), Subscription (sub-scope) or   Resource Group (rg-scope) scope.
    
    > While this template is **not** published, you can find the actual published modules in the subfolders
    > - `mg-scope`
    > - `sub-scope`
    > - `rg-scope`
    '''
    targetScope = 'managementGroup'

    updated with your module’s specifics

  • The tests/e2e folder MUST contain one instance of the require test cases per each scope, and MAY contain any additional test you see fit. In each case, the scope MUST be a prefix for the folder name. Each test case MUST reference the corresponding child module directly.

The full folder structure may look like

πŸ“„main.bicep                [Skeleton module with disclaimer referring to the child-modules]
πŸ“„main.json                 [ARM JSON file of the module]
πŸ“„README.md                 [The baseline readme, surfacing the metadata of the main.bicep file]
┣ πŸ“‚mg-scope
┃ β”£πŸ“„main.bicep             [Module deploying to mg-scope]
┃ β”£πŸ“„main.json              [ARM JSON file of the module]
┃ β”£πŸ“„README.md              [Readme of the module]
┃ β”£πŸ“„version.json           [Version file of the module]
┃ β”—πŸ“„CHANGELOG.md           [The changelog of the module]
┣ πŸ“‚sub-scope
┃ β”£πŸ“„main.bicep             [Module deploying to sub-scope]
┃ β”£πŸ“„main.json              [ARM JSON file of the module]
┃ β”£πŸ“„README.md              [Readme of the module]
┃ β”£πŸ“„version.json           [Version file of the module]
┃ β”—πŸ“„CHANGELOG.md           [The changelog of the module]
┣ πŸ“‚rg-scope
┃ β”£πŸ“„main.bicep             [Module deploying to rg-scope]
┃ β”£πŸ“„main.json              [ARM JSON file of the module]
┃ β”£πŸ“„README.md              [Readme of the module]
┃ β”£πŸ“„version.json           [Version file of the module]
┃ β”—πŸ“„CHANGELOG.md           [The changelog of the module]
β”— πŸ“‚tests/e2e
  ┣ πŸ“‚mg-scope.defaults
  ┃ β”—πŸ“„main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ πŸ“‚mg-scope.waf-aligned
  ┃ β”—πŸ“„main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ πŸ“‚mg-scope.max
  ┃ β”—πŸ“„main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ πŸ“‚sub-scope.defaults
  ┃ β”—πŸ“„main.test.bicep      [references the 'sub-scope' child module template: '../../../sub-scope/main.bicep']
  ┣ πŸ“‚sub-scope.waf-aligned
  ┃ β”—πŸ“„main.test.bicep      [references the 'sub-scope' child module template: '../../../sub-scope/main.bicep']
  ┣ πŸ“‚rg-scope.defaults
  ┃ β”—πŸ“„main.test.bicep      [references the 'rg-scope' child module template: '../../../rg-scope/main.bicep']
  β”— πŸ“‚rg-scope.waf-aligned
    β”—πŸ“„main.test.bicep      [references the 'rg-scope' child module template: '../../../rg-scope/main.bicep']
Important

Because each child-module is published on its own, you must ensure that each is registered in the MAR-file before the modules can be published. The MAR-file can only be accessed by Microsoft FTEs.

Please highlight the nature of your module in the issue when proposing it to AVM.




Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4RMFR6Parameter/Variable NamingMUSTOwnerContributorBAU
5RMFR7Minimum Required OutputsMUSTOwnerContributorBAU
6RMNFR2Parameter/Variable NamingMUSTOwnerContributorBAU
7BCPNFR1Complex data types - GeneralMUSTOwnerContributorBAU
8BCPNFR9Inputs - DecoratorsMUSTOwnerContributorBAU
9BCPNFR18User-defined types - SpecificationMUSTOwnerContributorBAU
10BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
11BCPNFR20User-defined types - ExportMUSTOwnerContributorBAU
12BCPNFR21User-defined types - DecoratorsMUSTOwnerContributorBAU
13BCPNFR7Parameter Requirement TypesMUSTOwnerContributorBAU
14BCPRMNFR2User-defined types - AVM-Common-TypesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR14 - Category: Inputs - Data Types

A module SHOULD use either: simple data types. e.g., string, int, bool.

OR

Complex data types (objects, arrays, maps) when the language-compliant schema is defined.




See origin...

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: RMFR6 - Category: Inputs - Parameter/Variable Naming

Parameters/variables that pertain to the primary resource MUST NOT use the resource type in the name.

e.g., use sku, vs. virtualMachineSku/virtualmachine_sku

Another example for where RPs contain some of their name within a property, leave the property unchanged. E.g. Key Vault has a property called keySize, it is fine to leave as this and not remove the key part from the property/parameter name.




See origin...

ID: RMFR7 - Category: Outputs - Minimum Required Outputs

Module owners MUST output the following outputs as a minimum in their modules:

OutputBicep Output NameTerraform Output Name
Resource Namenamename
Resource IDresourceIdresource_id
System Assigned Managed Identity Principal ID (if supported by module)systemAssignedMIPrincipalIdsystem_assigned_mi_principal_id
Tip

Module owners MAY also have to provide additional outputs depending on the IaC language, please check the language specific specs:




See origin...

ID: RMNFR2 - Category: Inputs - Parameter/Variable Naming

A resource module MUST use the following standard inputs:

  • name (no default)
  • location (if supported by the resource and not a global resource, then use Resource Group location, if resource supports Resource Groups, otherwise no default)



See origin...

ID: BCPNFR1 - Category: Inputs - Complex data types - General

To simplify the consumption experience for module consumers when interacting with complex data types input parameters, mainly objects and arrays, the Bicep features of Resource-Derived Types or User-Defined Types MUST be used and declared.

Tip

User-Defined Types are GA in Bicep as of version v0.21.1, Resource-Derived Types are GA as of version v0.34.1, please ensure you have this version(s) installed as a minimum.

Resource-Derived Types and User-Defined Types allow intellisense support in supported IDEs (e.g. Visual Studio Code) for complex input parameters using objects and array of objects.

v0.x Exemption

While we allow the release of major versions, starting with v1.0.0, retrofitting Resource-Derived Types and User-Defined Types for all modules will take a considerable amount of time.

Therefore, the addition of these features is currently NOT mandated/enforced. However, all modules MUST implement Resource-Derived Types and User-Defined Types prior to the release of their v1.0.0 version.




See origin...

ID: BCPNFR9 - Inputs - Decorators

Similar to BCPNFR21, input parameters MUST implement decorators such as description & secure (if sensitive).

Further, input parameters SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
param threshold: int?
@description('Required. The SKU of your resource.')
@allowed([
'Basic'
'Premium'
'Standard'
])
param sku string



See origin...

ID: BCPNFR18 - User-defined types - Specification

User-defined types (UDTs) MUST always be singular and non-nullable. The configuration of either should instead be done directly at the parameter or output that uses the type.

For example, instead of

param subnets subnetsType
type subnetsType = { ... }[]?

the type should be defined like

param subnets subnetType[]?
type subnetType = { ... }

The primary reason for this requirement is clarity. If not defined directly at the parameter or output, a user would always be required to check the type to understand how e.g., a parameter is expected.




See origin...

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct



See origin...

ID: BCPNFR20 - User-defined types - Export

User-defined types (UDTs) SHOULD always be exported via the @export() annotation in every template they’re implemented in.

@export()
type subnetType = { ... }

Doing so has the benefit that other (e.g., parent) modules can import them and as such reduce code duplication. Also, if the module itself is published, users of the Public Bicep Registry can import the types independently of the module itself. One example where this can be useful is a pattern module that may re-use the same interface when referencing a module from the registry.




See origin...

ID: BCPNFR21 - User-defined types - Decorators

Similar to BCPNFR9, User-defined types (UDTs) MUST implement decorators such as description & secure (if sensitive). This is true for every property of the UDT, as well as the UDT itself.

Further, User-defined types SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('My type''s description.')
type myType = {
  @description('Optional. The threshold of your resource.')
  @minValue(1)
  @maxValue(10)
  threshold: int?

  @description('Required. The SKU of your resource.')
  sku: ('Basic' | 'Premium' | 'Standard')
}



See origin...

ID: BCPNFR7 - Category: Inputs - Parameter Requirement Types

Modules will have lots of parameters that will differ in their requirement type (required, optional, etc.). To help consumers understand what each parameter’s requirement type is, module owners MUST add the requirement type to the beginning of each parameter’s description. Below are the requirement types with a definition and example for the description decorator:

Parameter Requirement TypeDefinitionExample Description Decorator
RequiredThe parameter value must be provided. The parameter does not have a default value and hence the module expects and requires an input.@description('Required. <PARAMETER DESCRIPTION HERE...>')
ConditionalThe parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. Should contain a sentence starting with ‘Required if (…).’ to explain the condition.@description('Conditional. <PARAMETER DESCRIPTION HERE...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe parameter value is generated within the module and should not be specified as input in most cases. A common example of this is the utcNow() function that is only supported as the input for a parameter value, and not inside a variable.@description('Generated. <PARAMETER DESCRIPTION HERE...>')



See origin...

ID: BCPRMNFR2 - User-defined types - AVM-Common-Types

When implementing any of the Bicep interface variants you MUST import their User-defined type (UDT) via the published AVM-Common-Types module.

When doing so, each type MUST be imported separately, right above the parameter or output that uses it.

import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. Array of role assignments to create.')
param roleAssignments roleAssignmentType[]?
import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. The diagnostic settings of the service.')
param diagnosticSettings diagnosticSettingFullType[]?

Importing them individually as opposed to one common block has several benefits such as

  • Individual versioning of types
  • If you must update the version for one type, you’re not exposed to unexpected changes to other types
Important

The import (...) block MUST not be added in between a parameter’s definition and its metadata. Doing so breaks the metadata’s binding to the parameter in question.

Finally, you should check for version updates regularly to ensure the resource module stays consistent with the specs. If the used AVM-Common-Types runs stale, the CI may eventually fail the module’s static tests.




Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9BCPNFR10Test Bicep File NamingMUSTOwnerContributorBAU
10BCPNFR11Test ToolingMUSTOwnerContributorBAU
11BCPNFR12Deployment Test NamingMUSTOwnerContributorBAU
12BCPNFR13Test file metadataMUSTOwnerContributorBAU
13BCPNFR16Post-deployment testsMUSTOwnerContributorBAU
14BCPRMNFR1Expected Test DirectoriesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).




See origin...

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.




See origin...

ID: SNFR5 - Category: Testing - Upgrade Tests

Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.




See origin...

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.




See origin...

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

ID: BCPNFR10 - Category: Testing - Test Bicep File Naming

Module owners MUST name their test .bicep files in the /tests/e2e/<defaults/waf-aligned/max/etc.> directories: main.test.bicep as the test framework (CI) relies upon this name.




See origin...

ID: BCPNFR11 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.

  • PSRule for Azure
  • Pester
    • Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Pester for your own tests.



See origin...

ID: BCPNFR12 - Category: Testing - Deployment Test Naming

Module owners MUST invoke the module in their test using the syntax:

module testDeployment '../../../main.bicep' =

Example 1: Working example with a single deployment

module testDeployment '../../../main.bicep' = {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}'
  params: {
    (...)
  }
}

Example 2: Working example using a deployment loop

@batchSize(1)
module testDeployment '../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}'
  params: {
    (...)
  }
}]

The syntax is used by the ReadMe-generating utility to identify, pull & format usage examples.




See origin...

ID: BCPNFR13 - Category: Testing - Test file metadata

By default, the ReadMe-generating utility will create usage examples headers based on each e2e folder’s name.
Module owners MAY provide a custom name & description by specifying the metadata blocks name & description in their main.test.bicep test files.

For example:

metadata name = 'Using Customer-Managed-Keys with System-Assigned identity'
metadata description = 'This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.'

would lead to a header in the module’s readme.md file along the lines of

### Example 1: _Using Customer-Managed-Keys with System-Assigned identity_

This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.



See origin...

ID: BCPNFR16 - Category: Testing - Post-deployment tests

For each test case in the e2e folder, you can optionally add post-deployment Pester tests that are executed once the corresponding deployment completed and before the removal logic kicks in.

To leverage the feature you MUST:

  • Use Pester as a test framework in each test file

  • Name the file with the suffix "*.tests.ps1"

  • Place each test file the e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file returns, as well as the path to the test template file in case you want to extract data from it directly.

    For example, with an output such as output resourceId string = testDeployment[1].outputs.resourceId defined in the main.test.bicep file, the $TestInputData would look like:

    $TestInputData = @{
      DeploymentOutputs    = @{
        resourceId = @{
          Type  = "String"
          Value = "/subscriptions/***/resourceGroups/dep-***-keyvault.vaults-kvvpe-rg/providers/Microsoft.KeyVault/vaults/***kvvpe001"
        }
      }
      ModuleTestFolderPath = "/home/runner/work/bicep-registry-modules/bicep-registry-modules/avm/res/key-vault/vault/tests/e2e/private-endpoint"
    }

    A full test file may look like:

    βž• Pester post-deployment test file example
    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )
    
    Describe 'Validate private endpoint deployment' {
    
        Context 'Validate sucessful deployment' {
    
            It "Private endpoints should be deployed in resource group" {
    
                $keyVaultResourceId = $TestInputData.DeploymentOutputs.resourceId.Value
                $testResourceGroup = ($keyVaultResourceId -split '\/')[4]
                $deployedPrivateEndpoints = Get-AzPrivateEndpoint -ResourceGroupName $testResourceGroup
                $deployedPrivateEndpoints.Count | Should -BeGreaterThan 0
            }
        }
    }



See origin...

ID: BCPRMNFR1 - Category: Testing - Expected Test Directories

Module owners MUST create the defaults, waf-aligned folders within their /tests/e2e/ directory in their resource module source code and SHOULD create a max folder also. Module owners CAN create additional folders as required. Each folder will be used as described for various test cases.

Note

If a module can deploy varying styles of the same resource, e.g., VMs can be Linux or Windows, each style should be tested as both defaults and waf-aligned. Each must then be used as suffixes in the directory name to denote the style, e.g., for a VM we would expect to see:

  • /tests/e2e/linux.defaults/main.test.bicep
  • /tests/e2e/linux.waf-aligned/main.test.bicep
  • /tests/e2e/windows.defaults/main.test.bicep
  • /tests/e2e/windows.waf-aligned/main.test.bicep

Defaults tests (MUST)

The defaults folder contains a test instance that deploys the module with the minimum set of required parameters.

This includes input parameters of type Required plus input parameters of type Conditional marked as required for WAF compliance.

This instance has heavy reliance on the default values for other input parameters. Parameters of type Optional SHOULD NOT be used.

WAF aligned tests (MUST)

The waf-aligned folder contains a test instance that deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.

This includes input parameters of type Required, parameters of type Conditional marked as required for WAF compliance, and parameters of type Optional useful for WAF compliance.

Parameters and dependencies which are not needed for WAF compliance, SHOULD NOT be included.

Max tests (SHOULD)

The max folder contains a test instance that deploys the module using a large parameter set, enabling most of the modules’ features.

The purpose of this instance is primarily parameter validation and not necessarily to serve as a real example scenario. Ideally, all features, extension resources and child resources should be enabled in this test, unless not possible due to conflicts, e.g., in case parameters are mutually exclusive.

Note

Please note that this test is not mandatory to have, but recommended for bulk parameter validation. It can be skipped in case the module parameter validation is covered already by additional, more scenario-specific tests.

Additional tests (CAN)

Additional folders CAN be created by module owners as required.

For example, to validate parameters not covered by the max test due to conflicts, or to provide a real example scenario for a specific use case.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3BCPNFR2Module Documentation GenerationMUSTOwnerContributorBAU
4BCPNFR3Usage Example formatsMUSTOwnerContributorBAU
5BCPNFR4Parameter Input ExamplesMAYOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR15 - Category: Documentation - Automatic Documentation Generation

README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: BCPNFR2 - Category: Documentation - Module Documentation Generation

Note

This script/tool is currently being developed by the AVM team and will be made available very soon.

Bicep modules documentation MUST be automatically generated via the provided script/tooling from the AVM team, providing the following headings:

  • Title
  • Description
  • Navigation
  • Resource Types
  • Usage Examples
  • Parameters
  • Outputs
  • Cross-referenced modules



See origin...

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <resourceName> 'br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<' = {
      name: '${uniqueString(deployment().name, location)}-test-<uniqueIdentifier>'
      params: { (...) }
    }
  • JSON / ARM Template Parameter Files - .json

    {
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
      "contentVersion": "1.0.0.0",
      "parameters": { (...) }
    }
Note

The above formats are currently automatically taken & generated from the tests/e2e tests. It is enough to run the Set-ModuleReadMe or Set-AVMModule functions (from the utilities folder) to update the usage examples in the readme(s).

Note

Bicep Parameter Files (.bicepparam) are being reviewed and considered by the AVM team for the usability and features at this time and will likely be added in the future.




See origin...

ID: BCPNFR4 - Category: Documentation - Parameter Input Examples

Bicep modules MAY provide parameter input examples for parameters using the metadata.example property via the @metadata() decorator.

Example:

@metadata({
  example: 'uksouth'
})
@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@metadata({
  example: '''
  {
    keyName: 'myKey'
    keyVaultResourceId: '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/myvault'
    keyVersion: '6d143c1a0a6a453daffec4001e357de0'
    userAssignedIdentityResourceId '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity'
  }
  '''
})
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyType

It is planned that these examples are automatically added to the module readme’s parameter descriptions when running either the Set-ModuleReadMe or Set-AVMModule scripts (available in the utilities folder).




Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
5BCPNFR22Bicep Module ChangelogMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.




See origin...

ID: BCPNFR22 - Category: Publishing - Changelog

When a module to be published (i.e., that has a version.json file) is changed, an entry MUST be created in the CHANGELOG.md file in the module folder. A link to the latest version of the changelog file has to be included at the top of the file, just below the # Changelog line. It is surrounded by empty lines.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/<ptn|res|utl>/<namespace/modulename[/submodulePath]>/CHANGELOG.md).

For each new version, an entry MUST be created above all existing versions in the CHANGELOG.md file of the module.

## <version>

### Changes

- This changed
- And this also

### Breaking Changes

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may be added as content for a section.
  • MUST exist only once.
  • All versions appear in descending order, which puts the most recent changes at the top.

What SHOULD be listed in the (Breaking) Changes section:

  • Relevant changes for the module
  • Changes in tests do not need to be added
Note

The versioning is following the SNFR17 - Semantic Versioning spec.

Example content of the CHANGELOG.md

A CHANGELOG.md file in the module’s root folder MUST start with the # Changelog header, followed by an empty line and a link to the latest published version of the changelog file, followed by another empty line. A section for each published version follows. Newer versions are placed above older versions.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/res/aad/domain-service/CHANGELOG.md).

## 0.2.1

### Changes

- Updated the referenced AVM common types

### Breaking Changes

- None

## 0.2.0

### Changes

- Implemented the minCPU parameter
- Updated the referenced VirtualNetwork module
- Updated the referenced AVM common types

### Breaking Changes

- The minCPU parameter is mandatory

## 0.1.0

### Changes

- Initial Release

### Breaking Changes

- None

Each bullet point should start with a capital letter.

Manual Editing

It is possible to modify the changelog content any time, e.g., to add missing versions, which will not create a new release of the module itself. Please note the following requirements in all cases:

  • All versions in the file, need to be valid and available as published version
  • Every version needs the two sections ## Changes and ## Breaking Changes with content
Note

Azure Verified Modules are artifacts in the Microsoft Container Registry (MCR). Every version of a module exists as a tag in the Container Registry and can be listed as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1BCPNFR8Code Styling - lower camelCasingSHOULDOwnerContributorBAU
2BCPNFR17Code Styling - Type castingSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: BCPNFR8 - Category: Composition - Code Styling - lower camelCasing

Module owners SHOULD use lower camelCasing for naming the following:

  • Parameters
  • Variables
  • Outputs
  • User Defined Types
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: camelCasingExample (lowercase first word (entirely), with capital of first letter of all other words and rest of word in lowercase)




See origin...

ID: BCPNFR17 - Category: Composition - Code Styling - Type casting

To improve the usability of primitive module properties declared as strings, you SHOULD declare them using a type which better represents them, and apply any required casting in the module on behalf of the user.

For reference, please refer to the following examples:

Boolean as String

Boolean as String
@allowed([
  'false'
  'true'
])
param myParameterValue string = 'false'

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: myParameterValue
  }
}
param myParameterValue string = false

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: string(myParameterValue)
  }
}

Integer Array as String Array

Integer Array as String Array
@allowed([
  '1'
  '2'
  '3'
])
param zones array

resource myResource '(...)' = {
  (...)
  properties: {
    zones: zones
  }
}
@allowed([
  1
  2
  3
])
param zones int[]

resource myResource '(...)' = {
  (...)
  properties: {
    zones: map(zones, zone => string(zone))
  }
}



Bicep Utility Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8BCPNFR15AVM Module Issue template fileMUSTOwnerBAU
βž• See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

ID: SNFR11 - Category: Contribution/Support - Issues Response Times

A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.




See origin...

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.




See origin...

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...



Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
3BCPFR4Telemetry EnablementMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}



See origin...

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.




See origin...

ID: BCPFR4 - Category: Composition - Telemetry Enablement

To comply with specifications outlined in SFR3 & SFR4 you MUST incorporate the following code snippet into your modules. Place this code sample in the “top level” main.bicep file; it is not necessary to include it in any nested Bicep files (child modules), unless they are marked for direct publishing (Ref Child module publishing).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: []
      outputs: {
        telemetry: {
          type: 'String'
          value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
        }
      }
    }
  }
}



Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SNFR25Resource NamingMUSTOwnerInitial
4UMNFR1Module NamingMUSTOwnerInitial
5BCPFR1Cross-Referencing ModulesMAYOwnerContributorBAU
6BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
7BCPNFR23Module compositionMUSTOwnerContributorBAU
8BCPNFR24Deterministic Deployment NamesMUSTOwnerContributorBAU
9BCPNFR14VersioningMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”



See origin...

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.




See origin...

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.




See origin...

ID: UMNFR1 - Category: Naming - Module Naming

Utility Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Utility Module Naming

  • Naming convention: avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name>
  • Example: avm/utl/general/get-environment or avm/utl/types/avm-common-types
  • Segments:
    • utl defines this as a utility module
    • <hyphenated grouping/category name> is a hierarchical grouping of utility modules by category, with each word separated by dashes, such as: general or types
    • <hyphenated utility module name> is a term describing the module’s function, with each word separated by dashes, e.g., get-environment = to get environmental details; avm-common-types = to use common types.

Terraform Utility Module Naming

  • Naming convention:
    • avm-utl-<utility module name> (Module name for registry)
    • terraform-<provider>-avm-utl-<utility module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-utl-sku-finder or avm-utl-naming
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • utl defines this as a utility module
    • <utility module name> is a term describing the module’s function, e.g., sku-finder = to find available SKUs; naming = to handle naming conventions.



See origin...

ID: BCPFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-reference other modules to build either Resource or Pattern modules.

However, they MUST be referenced only by a public registry reference to a pinned version e.g. br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<. They MUST NOT use local parent path references to a module e.g. ../../xxx/yyy.bicep.

The only exception to this rule are child modules as documented in BCPFR6.

Modules MUST NOT contain references to non-AVM modules.




See origin...

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct



See origin...

ID: BCPNFR23 - Category: Composition

Each Bicep AVM module that lives within the Azure/bicep-registry-modules (BRM) repository in the avm directory MUST have the following directories and files:

  • /tests - (for unit tests and additional E2E/integration if required - e.g. Pester etc.)
    • /e2e - (all examples must deploy successfully - these will be used to automatically generate the examples in the README.md for the module)
  • /src - (for scripts and other files - e.g., scripts used by the template)
    • exampleFile.ps1
  • /modules - (for sub-modules only if used and NOT children of the primary resource - e.g. RBAC role assignments)
    • exampleTemplate.bicep
  • /main.bicep (AVM Module main .bicep file and entry point/orchestration module)
  • /main.json (auto generated and what is published to the MCR via BRM)
  • /version.json (BRM requirement)
  • /README.md (auto generated AVM Module documentation)
  • /CHANGELOG.md (manually maintained changelog file with one entry per published version)

Directory and File Structure Example

/ Root of Azure/bicep-registry-modules
β”‚
β”œβ”€β”€β”€avm
β”‚   β”œβ”€β”€β”€ptn
β”‚   β”‚   └───apptiervmss
β”‚   β”‚       β”‚   main.bicep
β”‚   β”‚       β”‚   main.json
β”‚   β”‚       β”‚   README.md
β”‚   β”‚       β”‚   CHANGELOG.md
β”‚   β”‚       β”‚   version.json
β”‚   β”‚       β”œβ”€β”€β”€src (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€Get-Cake.ps1
β”‚   β”‚       β”‚   └───Find-Waldo.ps1
β”‚   β”‚       β”œβ”€β”€β”€modules (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€helper.bicep
β”‚   β”‚       β”‚   └───role-assignment.bicep
β”‚   β”‚       └───tests
β”‚   β”‚           β”œβ”€β”€β”€unit (optional)
β”‚   β”‚           └───e2e
β”‚   β”‚               β”œβ”€β”€β”€defaults
β”‚   β”‚               β”œβ”€β”€β”€waf-aligned
β”‚   β”‚               └───max
β”‚   β”‚
β”‚   └───res
β”‚       └───compute
β”‚           └───virtual-machine
β”‚               β”‚   main.bicep
β”‚               β”‚   main.json
β”‚               β”‚   README.md
β”‚               β”‚   CHANGELOG.md
β”‚               β”‚   version.json
β”‚               β”œβ”€β”€β”€src (optional)
β”‚               β”‚   β”œβ”€β”€β”€Set-Bug.ps1
β”‚               β”‚   └───Invoke-Promotion.ps1
β”‚               β”œβ”€β”€β”€modules (optional)
β”‚               β”‚   β”œβ”€β”€β”€helper.bicep
β”‚               β”‚   └───role-assignment.bicep
β”‚               └───tests
β”‚                   β”œβ”€β”€β”€unit (optional)
β”‚                   └───e2e
β”‚                       β”œβ”€β”€β”€defaults
β”‚                       β”œβ”€β”€β”€waf-aligned
β”‚                       └───max
β”œβ”€β”€β”€other repo dirs...
└───other repo files...



See origin...

ID: BCPNFR24 - Category: Naming/Composition - Deterministic Deployment Names

When a module references child, utility, or other modules, the deployment name MUST be deterministic. This means the deployment name must produce the same value for the same set of inputs across repeated deployments.

Why deterministic?

Azure Resource Manager has an 800-deployment limit per scope (resource group, subscription, management group, tenant). Non-deterministic names (e.g., those incorporating timestamps or utcNow()) create a new deployment object on every run, which can lead to this limit being reached over time.

While an automatic cleanup process exists for resource group and subscription scopes, it can take some time to take effect. Due to eventual consistency in the backend, the deployment count may not reflect the cleanup immediately, which can lead to failed deployments even when the actual number of deployments is below the 800 limit. Additionally, automatic cleanup does not apply to management group or tenant scopes.

We are actively working with the product team to enhance the cleanup process. In the meantime, deterministic deployment names provide a reliable way to keep deployment counts stable by overwriting previous deployment objects rather than creating new ones.

Deterministic deployment names cause Azure to overwrite the previous deployment object, keeping the deployment count stable regardless of how many times the module is deployed.

Requirement

Module owners MUST construct deployment names for referenced modules using uniqueString() seeded with the parent resource’s ID (<parentResource>.id) and location, rather than deployment().name, subscription().id, resourceGroup().id, utcNow(), or other non-deterministic or scope-level values.

The deployment name MUST follow the pattern:

'${uniqueString(<parentResource>.id, location)}-<ChildModuleDescriptor>-${index}'

Where:

SegmentDescription
uniqueString(<parentResource>.id, location)A deterministic hash derived from the parent resource’s resource ID and deployment location. This is both unique per resource instance and stable across deployments.
<ChildModuleDescriptor>A short, human-readable label identifying the child module being deployed (e.g., DB, Subnet, FederatedIdentityCred).
${index}The loop index variable, included when deploying in a loop. Omit for single (non-looped) deployments.
location parameter

If location is not available, for example when deploying a global resource that does not have a location property, it is acceptable to omit it. However, the <parentResource>.id MUST always be included as the primary seed for uniqueString.

Why parent resource ID?

Using the parent resource’s ID as the uniqueString seed provides two critical properties:

  1. Deterministic β€” the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. Collision-free β€” different parent resource instances produce different hashes, so deploying multiple instances of the same module type within the same scope does not cause naming collisions.
Why not subscription().id and resourceGroup().id separately?

The parent resource’s ID (e.g., /subscriptions/.../resourceGroups/.../providers/.../resourceName) already contains the subscription ID and resource group ID as segments. Using <parentResource>.id as a single input to uniqueString captures all of this context in one value, keeping the code concise and readable rather than passing multiple scope-level values separately.

Supporting multiple deployments of the same module at the same scope

A common scenario is deploying the same module type more than once within the same scope β€” for example, two different SQL servers each with their own set of databases, or two user-assigned identities each with their own federated credentials. Because the parent resource ID is unique per resource instance, the resulting deployment names will differ even when the child module type and index are identical. This ensures that parallel deployments of the same module at the same scope do not collide.

Other approaches fail on one or both of these properties:

ApproachDeterministic?Collision-free?Issue
deployment().nameβŒβœ…Changes every deployment; hits 800-limit
utcNow() / timestampsβŒβœ…Changes every deployment; hits 800-limit
subscription().id + resourceGroup().idβœ…βŒSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationβœ…βœ…Recommended β€” stable and unique per instance

Examples

Example 1: Single child module deployment

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_database 'database/main.bicep' = {
  name: '${uniqueString(server.id, location)}-Sql-DB'
  params: {
    serverName: server.name
    (...)
  }
}

Example 2: Child module deployment in a loop

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

ID: BCPNFR14 - Category: Composition - Versioning

To meet SNFR17 and depending on the changes you make, you may need to bump the version in the version.json file.

  {
    "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
    "version": "0.1"
  }
  

The version value is in the form of MAJOR.MINOR. The PATCH version will be incremented by the CI automatically when publishing the module to the Public Bicep Registry once the corresponding pull request is merged. Therefore, contributions that would only require an update of the patch version, can keep the version.json file intact.

For example, the version value should be:

  • 0.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4BCPNFR1Complex data types - GeneralMUSTOwnerContributorBAU
5BCPNFR9Inputs - DecoratorsMUSTOwnerContributorBAU
6BCPNFR18User-defined types - SpecificationMUSTOwnerContributorBAU
7BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
8BCPNFR20User-defined types - ExportMUSTOwnerContributorBAU
9BCPNFR21User-defined types - DecoratorsMUSTOwnerContributorBAU
10BCPNFR7Parameter Requirement TypesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR14 - Category: Inputs - Data Types

A module SHOULD use either: simple data types. e.g., string, int, bool.

OR

Complex data types (objects, arrays, maps) when the language-compliant schema is defined.




See origin...

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: BCPNFR1 - Category: Inputs - Complex data types - General

To simplify the consumption experience for module consumers when interacting with complex data types input parameters, mainly objects and arrays, the Bicep features of Resource-Derived Types or User-Defined Types MUST be used and declared.

Tip

User-Defined Types are GA in Bicep as of version v0.21.1, Resource-Derived Types are GA as of version v0.34.1, please ensure you have this version(s) installed as a minimum.

Resource-Derived Types and User-Defined Types allow intellisense support in supported IDEs (e.g. Visual Studio Code) for complex input parameters using objects and array of objects.

v0.x Exemption

While we allow the release of major versions, starting with v1.0.0, retrofitting Resource-Derived Types and User-Defined Types for all modules will take a considerable amount of time.

Therefore, the addition of these features is currently NOT mandated/enforced. However, all modules MUST implement Resource-Derived Types and User-Defined Types prior to the release of their v1.0.0 version.




See origin...

ID: BCPNFR9 - Inputs - Decorators

Similar to BCPNFR21, input parameters MUST implement decorators such as description & secure (if sensitive).

Further, input parameters SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
param threshold: int?
@description('Required. The SKU of your resource.')
@allowed([
'Basic'
'Premium'
'Standard'
])
param sku string



See origin...

ID: BCPNFR18 - User-defined types - Specification

User-defined types (UDTs) MUST always be singular and non-nullable. The configuration of either should instead be done directly at the parameter or output that uses the type.

For example, instead of

param subnets subnetsType
type subnetsType = { ... }[]?

the type should be defined like

param subnets subnetType[]?
type subnetType = { ... }

The primary reason for this requirement is clarity. If not defined directly at the parameter or output, a user would always be required to check the type to understand how e.g., a parameter is expected.




See origin...

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct



See origin...

ID: BCPNFR20 - User-defined types - Export

User-defined types (UDTs) SHOULD always be exported via the @export() annotation in every template they’re implemented in.

@export()
type subnetType = { ... }

Doing so has the benefit that other (e.g., parent) modules can import them and as such reduce code duplication. Also, if the module itself is published, users of the Public Bicep Registry can import the types independently of the module itself. One example where this can be useful is a pattern module that may re-use the same interface when referencing a module from the registry.




See origin...

ID: BCPNFR21 - User-defined types - Decorators

Similar to BCPNFR9, User-defined types (UDTs) MUST implement decorators such as description & secure (if sensitive). This is true for every property of the UDT, as well as the UDT itself.

Further, User-defined types SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('My type''s description.')
type myType = {
  @description('Optional. The threshold of your resource.')
  @minValue(1)
  @maxValue(10)
  threshold: int?

  @description('Required. The SKU of your resource.')
  sku: ('Basic' | 'Premium' | 'Standard')
}



See origin...

ID: BCPNFR7 - Category: Inputs - Parameter Requirement Types

Modules will have lots of parameters that will differ in their requirement type (required, optional, etc.). To help consumers understand what each parameter’s requirement type is, module owners MUST add the requirement type to the beginning of each parameter’s description. Below are the requirement types with a definition and example for the description decorator:

Parameter Requirement TypeDefinitionExample Description Decorator
RequiredThe parameter value must be provided. The parameter does not have a default value and hence the module expects and requires an input.@description('Required. <PARAMETER DESCRIPTION HERE...>')
ConditionalThe parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. Should contain a sentence starting with ‘Required if (…).’ to explain the condition.@description('Conditional. <PARAMETER DESCRIPTION HERE...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe parameter value is generated within the module and should not be specified as input in most cases. A common example of this is the utcNow() function that is only supported as the input for a parameter value, and not inside a variable.@description('Generated. <PARAMETER DESCRIPTION HERE...>')



Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8BCPNFR10Test Bicep File NamingMUSTOwnerContributorBAU
9BCPNFR11Test ToolingMUSTOwnerContributorBAU
10BCPNFR12Deployment Test NamingMUSTOwnerContributorBAU
11BCPNFR13Test file metadataMUSTOwnerContributorBAU
12BCPNFR16Post-deployment testsMUSTOwnerContributorBAU
13BCPRMNFR1Expected Test DirectoriesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).




See origin...

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.




See origin...

ID: SNFR5 - Category: Testing - Upgrade Tests

Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.




See origin...

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.




See origin...

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.




See origin...

ID: BCPNFR10 - Category: Testing - Test Bicep File Naming

Module owners MUST name their test .bicep files in the /tests/e2e/<defaults/waf-aligned/max/etc.> directories: main.test.bicep as the test framework (CI) relies upon this name.




See origin...

ID: BCPNFR11 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.

  • PSRule for Azure
  • Pester
    • Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Pester for your own tests.



See origin...

ID: BCPNFR12 - Category: Testing - Deployment Test Naming

Module owners MUST invoke the module in their test using the syntax:

module testDeployment '../../../main.bicep' =

Example 1: Working example with a single deployment

module testDeployment '../../../main.bicep' = {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}'
  params: {
    (...)
  }
}

Example 2: Working example using a deployment loop

@batchSize(1)
module testDeployment '../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}'
  params: {
    (...)
  }
}]

The syntax is used by the ReadMe-generating utility to identify, pull & format usage examples.




See origin...

ID: BCPNFR13 - Category: Testing - Test file metadata

By default, the ReadMe-generating utility will create usage examples headers based on each e2e folder’s name.
Module owners MAY provide a custom name & description by specifying the metadata blocks name & description in their main.test.bicep test files.

For example:

metadata name = 'Using Customer-Managed-Keys with System-Assigned identity'
metadata description = 'This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.'

would lead to a header in the module’s readme.md file along the lines of

### Example 1: _Using Customer-Managed-Keys with System-Assigned identity_

This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.



See origin...

ID: BCPNFR16 - Category: Testing - Post-deployment tests

For each test case in the e2e folder, you can optionally add post-deployment Pester tests that are executed once the corresponding deployment completed and before the removal logic kicks in.

To leverage the feature you MUST:

  • Use Pester as a test framework in each test file

  • Name the file with the suffix "*.tests.ps1"

  • Place each test file the e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file returns, as well as the path to the test template file in case you want to extract data from it directly.

    For example, with an output such as output resourceId string = testDeployment[1].outputs.resourceId defined in the main.test.bicep file, the $TestInputData would look like:

    $TestInputData = @{
      DeploymentOutputs    = @{
        resourceId = @{
          Type  = "String"
          Value = "/subscriptions/***/resourceGroups/dep-***-keyvault.vaults-kvvpe-rg/providers/Microsoft.KeyVault/vaults/***kvvpe001"
        }
      }
      ModuleTestFolderPath = "/home/runner/work/bicep-registry-modules/bicep-registry-modules/avm/res/key-vault/vault/tests/e2e/private-endpoint"
    }

    A full test file may look like:

    βž• Pester post-deployment test file example
    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )
    
    Describe 'Validate private endpoint deployment' {
    
        Context 'Validate sucessful deployment' {
    
            It "Private endpoints should be deployed in resource group" {
    
                $keyVaultResourceId = $TestInputData.DeploymentOutputs.resourceId.Value
                $testResourceGroup = ($keyVaultResourceId -split '\/')[4]
                $deployedPrivateEndpoints = Get-AzPrivateEndpoint -ResourceGroupName $testResourceGroup
                $deployedPrivateEndpoints.Count | Should -BeGreaterThan 0
            }
        }
    }



See origin...

ID: BCPRMNFR1 - Category: Testing - Expected Test Directories

Module owners MUST create the defaults, waf-aligned folders within their /tests/e2e/ directory in their resource module source code and SHOULD create a max folder also. Module owners CAN create additional folders as required. Each folder will be used as described for various test cases.

Note

If a module can deploy varying styles of the same resource, e.g., VMs can be Linux or Windows, each style should be tested as both defaults and waf-aligned. Each must then be used as suffixes in the directory name to denote the style, e.g., for a VM we would expect to see:

  • /tests/e2e/linux.defaults/main.test.bicep
  • /tests/e2e/linux.waf-aligned/main.test.bicep
  • /tests/e2e/windows.defaults/main.test.bicep
  • /tests/e2e/windows.waf-aligned/main.test.bicep

Defaults tests (MUST)

The defaults folder contains a test instance that deploys the module with the minimum set of required parameters.

This includes input parameters of type Required plus input parameters of type Conditional marked as required for WAF compliance.

This instance has heavy reliance on the default values for other input parameters. Parameters of type Optional SHOULD NOT be used.

WAF aligned tests (MUST)

The waf-aligned folder contains a test instance that deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.

This includes input parameters of type Required, parameters of type Conditional marked as required for WAF compliance, and parameters of type Optional useful for WAF compliance.

Parameters and dependencies which are not needed for WAF compliance, SHOULD NOT be included.

Max tests (SHOULD)

The max folder contains a test instance that deploys the module using a large parameter set, enabling most of the modules’ features.

The purpose of this instance is primarily parameter validation and not necessarily to serve as a real example scenario. Ideally, all features, extension resources and child resources should be enabled in this test, unless not possible due to conflicts, e.g., in case parameters are mutually exclusive.

Note

Please note that this test is not mandatory to have, but recommended for bulk parameter validation. It can be skipped in case the module parameter validation is covered already by additional, more scenario-specific tests.

Additional tests (CAN)

Additional folders CAN be created by module owners as required.

For example, to validate parameters not covered by the max test due to conflicts, or to provide a real example scenario for a specific use case.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3BCPNFR2Module Documentation GenerationMUSTOwnerContributorBAU
4BCPNFR3Usage Example formatsMUSTOwnerContributorBAU
5BCPNFR4Parameter Input ExamplesMAYOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR15 - Category: Documentation - Automatic Documentation Generation

README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: BCPNFR2 - Category: Documentation - Module Documentation Generation

Note

This script/tool is currently being developed by the AVM team and will be made available very soon.

Bicep modules documentation MUST be automatically generated via the provided script/tooling from the AVM team, providing the following headings:

  • Title
  • Description
  • Navigation
  • Resource Types
  • Usage Examples
  • Parameters
  • Outputs
  • Cross-referenced modules



See origin...

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <resourceName> 'br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<' = {
      name: '${uniqueString(deployment().name, location)}-test-<uniqueIdentifier>'
      params: { (...) }
    }
  • JSON / ARM Template Parameter Files - .json

    {
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
      "contentVersion": "1.0.0.0",
      "parameters": { (...) }
    }
Note

The above formats are currently automatically taken & generated from the tests/e2e tests. It is enough to run the Set-ModuleReadMe or Set-AVMModule functions (from the utilities folder) to update the usage examples in the readme(s).

Note

Bicep Parameter Files (.bicepparam) are being reviewed and considered by the AVM team for the usability and features at this time and will likely be added in the future.




See origin...

ID: BCPNFR4 - Category: Documentation - Parameter Input Examples

Bicep modules MAY provide parameter input examples for parameters using the metadata.example property via the @metadata() decorator.

Example:

@metadata({
  example: 'uksouth'
})
@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@metadata({
  example: '''
  {
    keyName: 'myKey'
    keyVaultResourceId: '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/myvault'
    keyVersion: '6d143c1a0a6a453daffec4001e357de0'
    userAssignedIdentityResourceId '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity'
  }
  '''
})
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyType

It is planned that these examples are automatically added to the module readme’s parameter descriptions when running either the Set-ModuleReadMe or Set-AVMModule scripts (available in the utilities folder).




Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
5BCPNFR22Bicep Module ChangelogMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.




See origin...

ID: BCPNFR22 - Category: Publishing - Changelog

When a module to be published (i.e., that has a version.json file) is changed, an entry MUST be created in the CHANGELOG.md file in the module folder. A link to the latest version of the changelog file has to be included at the top of the file, just below the # Changelog line. It is surrounded by empty lines.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/<ptn|res|utl>/<namespace/modulename[/submodulePath]>/CHANGELOG.md).

For each new version, an entry MUST be created above all existing versions in the CHANGELOG.md file of the module.

## <version>

### Changes

- This changed
- And this also

### Breaking Changes

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may be added as content for a section.
  • MUST exist only once.
  • All versions appear in descending order, which puts the most recent changes at the top.

What SHOULD be listed in the (Breaking) Changes section:

  • Relevant changes for the module
  • Changes in tests do not need to be added
Note

The versioning is following the SNFR17 - Semantic Versioning spec.

Example content of the CHANGELOG.md

A CHANGELOG.md file in the module’s root folder MUST start with the # Changelog header, followed by an empty line and a link to the latest published version of the changelog file, followed by another empty line. A section for each published version follows. Newer versions are placed above older versions.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/res/aad/domain-service/CHANGELOG.md).

## 0.2.1

### Changes

- Updated the referenced AVM common types

### Breaking Changes

- None

## 0.2.0

### Changes

- Implemented the minCPU parameter
- Updated the referenced VirtualNetwork module
- Updated the referenced AVM common types

### Breaking Changes

- The minCPU parameter is mandatory

## 0.1.0

### Changes

- Initial Release

### Breaking Changes

- None

Each bullet point should start with a capital letter.

Manual Editing

It is possible to modify the changelog content any time, e.g., to add missing versions, which will not create a new release of the module itself. Please note the following requirements in all cases:

  • All versions in the file, need to be valid and available as published version
  • Every version needs the two sections ## Changes and ## Breaking Changes with content
Note

Azure Verified Modules are artifacts in the Microsoft Container Registry (MCR). Every version of a module exists as a tag in the Container Registry and can be listed as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1BCPNFR8Code Styling - lower camelCasingSHOULDOwnerContributorBAU
2BCPNFR17Code Styling - Type castingSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: BCPNFR8 - Category: Composition - Code Styling - lower camelCasing

Module owners SHOULD use lower camelCasing for naming the following:

  • Parameters
  • Variables
  • Outputs
  • User Defined Types
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: camelCasingExample (lowercase first word (entirely), with capital of first letter of all other words and rest of word in lowercase)




See origin...

ID: BCPNFR17 - Category: Composition - Code Styling - Type casting

To improve the usability of primitive module properties declared as strings, you SHOULD declare them using a type which better represents them, and apply any required casting in the module on behalf of the user.

For reference, please refer to the following examples:

Boolean as String

Boolean as String
@allowed([
  'false'
  'true'
])
param myParameterValue string = 'false'

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: myParameterValue
  }
}
param myParameterValue string = false

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: string(myParameterValue)
  }
}

Integer Array as String Array

Integer Array as String Array
@allowed([
  '1'
  '2'
  '3'
])
param zones array

resource myResource '(...)' = {
  (...)
  properties: {
    zones: zones
  }
}
@allowed([
  1
  2
  3
])
param zones int[]

resource myResource '(...)' = {
  (...)
  properties: {
    zones: map(zones, zone => string(zone))
  }
}



Terraform Specifications

Specifications by Category and Module Classification

CategoryResourcePatternUtility
Contribution/Support988
Telemetry222
Naming/Composition17127
CodeStyle292929
Inputs/Outputs865
Testing10109
Documentation444
Release/Publishing444
Summary837568

How to propose changes to the specifications?

Important

Any updates to existing or new specifications for Terraform must be submitted as a draft for review by Azure Terraform PG/Engineering(@Azure/terraform-avm) and AVM core team(@Azure/avm-core-team).

Important

Provider Versatility: Users have the autonomy to choose between AzureRM, AzAPI, or a combination of both, tailored to the specific complexity of module requirements.

What changed recently?

No specifications were changed in the last 30 days.

Subsections of Terraform

Terraform Interfaces

This chapter details the interfaces/schemas for the AVM Resource Modules features/extension resources as referenced in RMFR4 and RMFR5.

Diagnostic Settings

Important

Allowed values for logs and metric categories or category groups MUST NOT be specified to keep the module implementation evergreen for any new categories or category groups added by RPs, without module owners having to update a list of allowed values and cut a new release of their module.

  variable "diagnostic_settings" {
    type = map(object({
      name                                     = optional(string, null)
      log_categories                           = optional(set(string), [])
      log_groups                               = optional(set(string), ["allLogs"])
      metric_categories                        = optional(set(string), ["AllMetrics"])
      log_analytics_destination_type           = optional(string, "Dedicated")
      workspace_resource_id                    = optional(string, null)
      storage_account_resource_id              = optional(string, null)
      event_hub_authorization_rule_resource_id = optional(string, null)
      event_hub_name                           = optional(string, null)
      marketplace_partner_resource_id          = optional(string, null)
    }))
    default  = {}
    nullable = false
  
    validation {
      condition     = alltrue([for _, v in var.diagnostic_settings : contains(["Dedicated", "AzureDiagnostics"], v.log_analytics_destination_type)])
      error_message = "Log analytics destination type must be one of: 'Dedicated', 'AzureDiagnostics'."
    }
    validation {
      condition = alltrue(
        [
          for _, v in var.diagnostic_settings :
          v.workspace_resource_id != null || v.storage_account_resource_id != null || v.event_hub_authorization_rule_resource_id != null || v.marketplace_partner_resource_id != null
        ]
      )
      error_message = "At least one of `workspace_resource_id`, `storage_account_resource_id`, `marketplace_partner_resource_id`, or `event_hub_authorization_rule_resource_id`, must be set."
    }
    description = <<DESCRIPTION
  A map of diagnostic settings to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  
  - `name` - (Optional) The name of the diagnostic setting. One will be generated if not set, however this will not be unique if you want to create multiple diagnostic setting resources.
  - `log_categories` - (Optional) A set of log categories to send to the log analytics workspace. Defaults to `[]`.
  - `log_groups` - (Optional) A set of log groups to send to the log analytics workspace. Defaults to `["allLogs"]`.
  - `metric_categories` - (Optional) A set of metric categories to send to the log analytics workspace. Defaults to `["AllMetrics"]`.
  - `log_analytics_destination_type` - (Optional) The destination type for the diagnostic setting. Possible values are `Dedicated` and `AzureDiagnostics`. Defaults to `Dedicated`.
  - `workspace_resource_id` - (Optional) The resource ID of the log analytics workspace to send logs and metrics to.
  - `storage_account_resource_id` - (Optional) The resource ID of the storage account to send logs and metrics to.
  - `event_hub_authorization_rule_resource_id` - (Optional) The resource ID of the event hub authorization rule to send logs and metrics to.
  - `event_hub_name` - (Optional) The name of the event hub. If none is specified, the default event hub will be selected.
  - `marketplace_partner_resource_id` - (Optional) The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic LogsLogs.
  DESCRIPTION
  }
  
  # Sample resource
  resource "azurerm_monitor_diagnostic_setting" "this" {
    for_each                       = var.diagnostic_settings
    name                           = each.value.name != null ? each.value.name : "diag-${var.name}"
    target_resource_id             = azurerm_<MY_RESOURCE>.this.id
    storage_account_id             = each.value.storage_account_resource_id
    eventhub_authorization_rule_id = each.value.event_hub_authorization_rule_resource_id
    eventhub_name                  = each.value.event_hub_name
    partner_solution_id            = each.value.marketplace_partner_resource_id
    log_analytics_workspace_id     = each.value.workspace_resource_id
    log_analytics_destination_type = each.value.log_analytics_destination_type
  
    dynamic "enabled_log" {
      for_each = each.value.log_categories
      content {
        category = enabled_log.value
      }
    }
  
    dynamic "enabled_log" {
      for_each = each.value.log_groups
      content {
        category_group = enabled_log.value
      }
    }
  
    dynamic "enabled_metric" {
      for_each = each.value.metric_categories
      content {
        category = enabled_metric.value
      }
    }
  }
  
  diagnostic_settings = {
    diag_setting_1 = {
      name                                     = "diagSetting1"
      log_groups                               = ["allLogs"]
      metric_categories                        = ["AllMetrics"]
      log_analytics_destination_type           = "Dedicated"
      workspace_resource_id                    = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}"
      storage_account_resource_id              = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}"
      event_hub_authorization_rule_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}"
      event_hub_name                           = "{eventHubName}"
      marketplace_partner_resource_id          = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}"
    }
  }
  
Note

In the provided example for Diagnostic Settings, both logs and metrics are enabled for the associated resource. However, it is IMPORTANT to note that certain resources may not support both diagnostic setting types/categories. In such cases, the resource configuration MUST be modified accordingly to ensure proper functionality and compliance with system requirements.

Role Assignments

  variable "role_assignments" {
    type = map(object({
      role_definition_id_or_name             = string
      principal_id                           = string
      description                            = optional(string, null)
      skip_service_principal_aad_check       = optional(bool, false)
      condition                              = optional(string, null)
      condition_version                      = optional(string, null)
      delegated_managed_identity_resource_id = optional(string, null)
      principal_type                         = optional(string, null)
    }))
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  A map of role assignments to create on the <RESOURCE>. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  
  - `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
  - `principal_id` - The ID of the principal to assign the role to.
  - `description` - (Optional) The description of the role assignment.
  - `skip_service_principal_aad_check` - (Optional) If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.
  - `condition` - (Optional) The condition which will be used to scope the role assignment.
  - `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
  - `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
  - `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
  
  > Note: only set `skip_service_principal_aad_check` to true if you are assigning a role to a service principal.
  DESCRIPTION
  }
  
  locals {
    role_definition_resource_substring = "providers/Microsoft.Authorization/roleDefinitions"
  }
  
  # Example resource declaration
  resource "azurerm_role_assignment" "this" {
    for_each                               = var.role_assignments
    scope                                  = azurerm_MY_RESOURCE.this.id
    role_definition_id                     = strcontains(lower(each.value.role_definition_id_or_name), lower(local.role_definition_resource_substring)) ? each.value.role_definition_id_or_name : null
    role_definition_name                   = strcontains(lower(each.value.role_definition_id_or_name), lower(local.role_definition_resource_substring)) ? null : each.value.role_definition_id_or_name
    principal_id                           = each.value.principal_id
    condition                              = each.value.condition
    condition_version                      = each.value.condition_version
    skip_service_principal_aad_check       = each.value.skip_service_principal_aad_check
    delegated_managed_identity_resource_id = each.value.delegated_managed_identity_resource_id
    principal_type                         = each.value.principal_type
  }
  
  role_assignments = {
    role_assignment_1 = {
      role_definition_id_or_name             = "Contributor"
      principal_id                           = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      skip_service_principal_aad_check       = true
    },
    role_assignment_2 = {
      role_definition_id_or_name             = "Storage Blob Data Reader"
      principal_id                           = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
      description                            = "Example role assignment 2 of reader role"
      skip_service_principal_aad_check       = false
      condition                              = "@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase 'foo_storage_container'"
      condition_version                      = "2.0"
    }
  }
  

Details on child, extension and cross-referenced resources:

  • Modules MUST support Role Assignments on child, extension and cross-referenced resources as well as the primary resource via parameters/variables

Resource Locks

  variable "lock" {
    type = object({
      kind = string
      name = optional(string, null)
    })
    default     = null
    description = <<DESCRIPTION
  Controls the Resource Lock configuration for this resource. The following properties can be specified:
  
  - `kind` - (Required) The type of lock. Possible values are `\"CanNotDelete\"` and `\"ReadOnly\"`.
  - `name` - (Optional) The name of the lock. If not specified, a name will be generated based on the `kind` value. Changing this forces the creation of a new resource.
  DESCRIPTION
  
    validation {
      condition     = var.lock != null ? contains(["CanNotDelete", "ReadOnly"], var.lock.kind) : true
      error_message = "Lock kind must be either `\"CanNotDelete\"` or `\"ReadOnly\"`."
    }
  }
  
  # Example resource implementation
  resource "azurerm_management_lock" "this" {
    count = var.lock != null ? 1 : 0
  
    lock_level = var.lock.kind
    name       = coalesce(var.lock.name, "lock-${var.lock.kind}")
    scope      = azurerm_MY_RESOURCE.this.id
    notes      = var.lock.kind == "CanNotDelete" ? "Cannot delete the resource or its child resources." : "Cannot delete or modify the resource or its child resources."
  }
  
  lock = {
    name = "lock-{resourcename}" # optional
    type = "CanNotDelete"
  }
  

Details on child and extension resources:

  • Locks SHOULD be able to be set for child resources of the primary resource in resource modules

Details on cross-referenced resources:

  • Locks MUST be automatically applied to cross-referenced resources if the primary resource has a lock applied.
    • This MUST also be able to be turned off for each of the cross-referenced resources by the module consumer via a parameter/variable if they desire

An example of this is a Key Vault module that has a Private Endpoints enabled. If a lock is applied to the Key Vault via the lock parameter/variable then the lock should also be applied to the Private Endpoint automatically, unless the privateEndpointLock/private_endpoint_lock (example name) parameter/variable is set to None

Important

In Terraform, locks become part of the resource graph and suitable depends_on values should be set. Note that, during a destroy operation, Terraform will remove the locks before removing the resource itself, reducing the usefulness of the lock somewhat. Also note, due to eventual consistency in Azure, use of locks can cause destroy operations to fail as the lock may not have been fully removed by the time the destroy operation is executed.

Tags

  variable "tags" {
    type     = map(string)
    default  = null
    description = "(Optional) Tags of the resource."
  }
  
  tags = {
    key           = "value"
    "another-key" = "another-value"
    integers      = 123
  }
  

Details on child, extension and cross-referenced resources:

  • Tags MUST be automatically applied to child, extension and cross-referenced resources, if tags are applied to the primary resource.
    • By default, all tags set for the primary resource will automatically be passed down to child, extension and cross-referenced resources.
    • This MUST be able to be overridden by the module consumer so they can specify alternate tags for child, extension and cross-referenced resources, if they desire via a parameter/variable
      • If overridden by the module consumer, no merge/union of tags will take place from the primary resource and only the tags specified for the child, extension and cross-referenced resources will be applied

Managed Identities

  variable "managed_identities" {
    type = object({
      system_assigned            = optional(bool, false)
      user_assigned_resource_ids = optional(set(string), [])
    })
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  Controls the Managed Identity configuration on this resource. The following properties can be specified:
  
  - `system_assigned` - (Optional) Specifies if the System Assigned Managed Identity should be enabled.
  - `user_assigned_resource_ids` - (Optional) Specifies a list of User Assigned Managed Identity resource IDs to be assigned to this resource.
  DESCRIPTION
  }
  
  # Helper locals to make the dynamic block more readable
  # There are three attributes here to cater for resources that
  # support both user and system MIs, only system MIs, and only user MIs
  locals {
    managed_identities = {
      system_assigned_user_assigned = (var.managed_identities.system_assigned || length(var.managed_identities.user_assigned_resource_ids) > 0) ? {
        this = {
          type                       = var.managed_identities.system_assigned && length(var.managed_identities.user_assigned_resource_ids) > 0 ? "SystemAssigned, UserAssigned" : length(var.managed_identities.user_assigned_resource_ids) > 0 ? "UserAssigned" : "SystemAssigned"
          user_assigned_resource_ids = var.managed_identities.user_assigned_resource_ids
        }
      } : {}
      system_assigned = var.managed_identities.system_assigned ? {
        this = {
          type = "SystemAssigned"
        }
      } : {}
      user_assigned = length(var.managed_identities.user_assigned_resource_ids) > 0 ? {
        this = {
          type                       = "UserAssigned"
          user_assigned_resource_ids = var.managed_identities.user_assigned_resource_ids
        }
      } : {}
    }
  }
  
  ## Resources supporting both SystemAssigned and UserAssigned
  dynamic "identity" {
    for_each = local.managed_identities.system_assigned_user_assigned
    content {
      type         = identity.value.type
      identity_ids = identity.value.user_assigned_resource_ids
    }
  }
  
  ## Resources that only support SystemAssigned
  dynamic "identity" {
    for_each = identity.managed_identities.system_assigned
    content {
      type = identity.value.type
    }
  }
  
  ## Resources that only support UserAssigned
  dynamic "identity" {
    for_each = local.managed_identities.user_assigned
    content {
      type         = identity.value.type
      identity_ids = identity.value.user_assigned_resource_ids
    }
  }
  
  managed_identities = {
    system_assigned = true
    user_assigned_resource_ids = [
      "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}",
      "/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}"
    ]
  }
  

Reason for differences in User Assigned data type in languages:

  • We do not forsee the Managed Identity Resource Provider team to ever add additional properties within the empty object ({}) value required on the input of a User Assigned Managed Identity.
  • In Bicep we therefore have removed the need for this to be declared and just converted it to a simple array of Resource IDs
  • However, in Terraform we have left it as a object/map as this simplifies for_each and other loop mechanisms and provides more consistency in plan, apply, destroy operations
    • Especially when adding, removing or changing the order of the User Assigned Managed Identities as they are declared

Private Endpoints

  # In this example we only support one service, e.g. Key Vault.
  # If your service has multiple private endpoint services, then expose the service name.
  
  # This variable is used to determine if the private_dns_zone_group block should be included,
  # or if it is to be managed externally, e.g. using Azure Policy.
  # https://github.com/Azure/terraform-azurerm-avm-res-keyvault-vault/issues/32
  # Alternatively you can use AzAPI, which does not have this issue.
  variable "private_endpoints_manage_dns_zone_group" {
    type        = bool
    default     = true
    nullable    = false
    description = "Whether to manage private DNS zone groups with this module. If set to false, you must manage private DNS zone groups externally, e.g. using Azure Policy."
  }
  
  variable "private_endpoints" {
    type = map(object({
      name               = optional(string, null)
      role_assignments   = optional(map(object({
        role_definition_id_or_name             = string
        principal_id                           = string
        description                            = optional(string, null)
        skip_service_principal_aad_check       = optional(bool, false)
        condition                              = optional(string, null)
        condition_version                      = optional(string, null)
        delegated_managed_identity_resource_id = optional(string, null)
        principal_type         							   = optional(string, null)
      })), {})
      lock               = optional(object({
        kind = string
        name = optional(string, null)
      }), null)
      tags               = optional(map(string), null)
      subnet_resource_id = string
      subresource_name   = string  # NOTE: `subresource_name` can be excluded if the resource does not support multiple sub resource types (e.g. storage account supports blob, queue, etc)
      private_dns_zone_group_name             = optional(string, "default")
      private_dns_zone_resource_ids           = optional(set(string), [])
      application_security_group_associations = optional(map(string), {})
      private_service_connection_name         = optional(string, null)
      network_interface_name                  = optional(string, null)
      location                                = optional(string, null)
      resource_group_name                     = optional(string, null)
      ip_configurations = optional(map(object({
        name               = string
        private_ip_address = string
      })), {})
    }))
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  A map of private endpoints to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  
  - `name` - (Optional) The name of the private endpoint. One will be generated if not set.
  - `role_assignments` - (Optional) A map of role assignments to create on the private endpoint. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. See `var.role_assignments` for more information.
    - `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
    - `principal_id` - The ID of the principal to assign the role to.
    - `description` - (Optional) The description of the role assignment.
    - `skip_service_principal_aad_check` - (Optional) If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.
    - `condition` - (Optional) The condition which will be used to scope the role assignment.
    - `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
    - `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
    - `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
  - `lock` - (Optional) The lock level to apply to the private endpoint. Default is `None`. Possible values are `None`, `CanNotDelete`, and `ReadOnly`.
    - `kind` - (Required) The type of lock. Possible values are `\"CanNotDelete\"` and `\"ReadOnly\"`.
    - `name` - (Optional) The name of the lock. If not specified, a name will be generated based on the `kind` value. Changing this forces the creation of a new resource.
  - `tags` - (Optional) A mapping of tags to assign to the private endpoint.
  - `subnet_resource_id` - The resource ID of the subnet to deploy the private endpoint in.
  - `subresource_name` - The name of the sub resource for the private endpoint.
  - `private_dns_zone_group_name` - (Optional) The name of the private DNS zone group. One will be generated if not set.
  - `private_dns_zone_resource_ids` - (Optional) A set of resource IDs of private DNS zones to associate with the private endpoint. If not set, no zone groups will be created and the private endpoint will not be associated with any private DNS zones. DNS records must be managed external to this module.
  - `application_security_group_resource_ids` - (Optional) A map of resource IDs of application security groups to associate with the private endpoint. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  - `private_service_connection_name` - (Optional) The name of the private service connection. One will be generated if not set.
  - `network_interface_name` - (Optional) The name of the network interface. One will be generated if not set.
  - `location` - (Optional) The Azure location where the resources will be deployed. Defaults to the location of the resource group.
  - `resource_group_name` - (Optional) The resource group where the resources will be deployed. Defaults to the resource group of the Key Vault.
  - `ip_configurations` - (Optional) A map of IP configurations to create on the private endpoint. If not specified the platform will create one. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
    - `name` - The name of the IP configuration.
    - `private_ip_address` - The private IP address of the IP configuration.
  DESCRIPTION
  }
  
  # The PE resource when we are managing the private_dns_zone_group block:
  resource "azurerm_private_endpoint" "this" {
    for_each                      = { for k, v in var.private_endpoints : k => v if var.private_endpoints_manage_dns_zone_group }
    name                          = each.value.name != null ? each.value.name : "pep-${var.name}"
    location                      = each.value.location != null ? each.value.location : var.location
    resource_group_name           = each.value.resource_group_name != null ? each.value.resource_group_name : var.resource_group_name
    subnet_id                     = each.value.subnet_resource_id
    custom_network_interface_name = each.value.network_interface_name
    tags                          = each.value.tags
  
    private_service_connection {
      name                           = each.value.private_service_connection_name != null ? each.value.private_service_connection_name : "pse-${var.name}"
      private_connection_resource_id = azurerm_key_vault.this.id
      is_manual_connection           = false
      subresource_names              = ["MYSERVICE"] # map to each.value.subresource_name if there are multiple services.
    }
  
    dynamic "private_dns_zone_group" {
      for_each = length(each.value.private_dns_zone_resource_ids) > 0 ? ["this"] : []
  
      content {
        name                 = each.value.private_dns_zone_group_name
        private_dns_zone_ids = each.value.private_dns_zone_resource_ids
      }
    }
  
    dynamic "ip_configuration" {
      for_each = each.value.ip_configurations
  
      content {
        name               = ip_configuration.value.name
        subresource_name   = "MYSERVICE" # map to each.value.subresource_name if there are multiple services.
        member_name        = "MYSERVICE" # map to each.value.subresource_name if there are multiple services.
        private_ip_address = ip_configuration.value.private_ip_address
      }
    }
  }
  
  # The PE resource when we are managing **not** the private_dns_zone_group block:
  resource "azurerm_private_endpoint" "this_unmanaged_dns_zone_groups" {
    for_each = { for k, v in var.private_endpoints : k => v if !var.private_endpoints_manage_dns_zone_group }
  
    # ... repeat configuration above
    # **omitting the private_dns_zone_group block**
    # then add the following lifecycle block to ignore changes to the private_dns_zone_group block
  
    lifecycle {
      ignore_changes = [private_dns_zone_group]
    }
  }
  
  # Private endpoint application security group associations.
  # We merge the nested maps from private endpoints and application security group associations into a single map.
  locals {
    private_endpoint_application_security_group_associations = { for assoc in flatten([
      for pe_k, pe_v in var.private_endpoints : [
        for asg_k, asg_v in pe_v.application_security_group_associations : {
          asg_key         = asg_k
          pe_key          = pe_k
          asg_resource_id = asg_v
        }
      ]
    ]) : "${assoc.pe_key}-${assoc.asg_key}" => assoc }
  }
  
  resource "azurerm_private_endpoint_application_security_group_association" "this" {
    for_each                      = local.private_endpoint_application_security_group_associations
    private_endpoint_id           = azurerm_private_endpoint.this[each.value.pe_key].id
    application_security_group_id = each.value.asg_resource_id
  }
  
  # You need an additional resource when not managing private_dns_zone_group with this module:
  
  # In your output you need to select the correct resource based on the value of var.private_endpoints_manage_dns_zone_group:
  output "private_endpoints" {
    value       = var.private_endpoints_manage_dns_zone_group ? azurerm_private_endpoint.this : azurerm_private_endpoint.this_unmanaged_dns_zone_groups
    description = <<DESCRIPTION
  A map of the private endpoints created.
  DESCRIPTION
  }
  
  private_endpoints = {
    pe1 = {
      role_assignments   = {} # see interfaces/role assignments
      lock               = {} # see interfaces/resource locks
      tags               = {} # see interfaces/tags
      subnet_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}"
      private_dns_zone_resource_ids = [
        "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/privateDnsZones/{dnsZoneName}"
      ]
      application_security_group_associations = {
        asg1 = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/applicationSecurityGroups/{asgName}"
      }
      network_interface_name = "nic1"
      ip_configurations = {
        ipconfig1 = {
          name               = "ipconfig1"
          group_id           = "vault"
          member_name        = "default"
          private_ip_address = "10.0.0.7"
        }
      }
    }
  }
  

Notes:

  • The properties defined in the schema above are the minimum amount of properties expected to be exposed for Private Endpoints in AVM Resource Modules.
    • A module owner MAY chose to expose additional properties of the Private Endpoint resource.
      • However, module owners considering this SHOULD contact the AVM core team first to consult on how the property should be exposed to avoid future breaking changes to the schema that may be enforced upon them.
  • Module owners MAY chose to define a list of allowed value for the ‘service’ (a.k.a. groupIds) property.
    • However, they should do so with caution as should a new service appear for their resource module, a new release will need to be cut to add this new service to the allowed values.
      • Whereas not specifying allowed values will allow flexibility from day 0 without the need for any changes and releases to be made.

Customer Managed Keys

  variable "customer_managed_key" {
    type = object({
      key_vault_resource_id  = string
      key_name               = string
      key_version            = optional(string, null)
      user_assigned_identity = optional(object({
        resource_id = string
      }), null)
    })
    default = null
  }
  
  customer_managed_key = {
    key_vault_resource_id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}"
    key_name: "{keyName}"
    key_version: "{keyVersion}"
    user_assigned_identity_resource_id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}"
  }
  

Azure Monitor Alerts

Note

This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.

Terraform Pattern Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8TFNFR3GitHub Repo Branch ProtectionMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

ID: SNFR11 - Category: Contribution/Support - Issues Response Times

A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.




See origin...

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.




See origin...

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.




Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
βž• See Specifications for this category
See origin...

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}



See origin...

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.




Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6PMFR1Resource Group CreationMAYOwnerContributorBAU
7PMNFR1Module NamingMUSTOwnerInitial
8PMNFR2Use Resource Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
9PMNFR3Use other Pattern Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
10TFFR1Cross-Referencing ModulesMUSTOwnerContributorBAU
11TFFR3Providers - Permitted VersionsMUSTOwnerContributorBAU
12TFNFR4Lower snake_casingMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”



See origin...

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.




See origin...

ID: PMFR1 - Category: Composition - Resource Group Creation

A Pattern Module MAY create Resource Group(s).




See origin...

ID: PMNFR1 - Category: Naming - Module Naming

Pattern Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Pattern Module Naming

  • Naming convention: avm/ptn/<hyphenated grouping/category name>/<hyphenated pattern module name>
  • Example: avm/ptn/compute/app-tier-vmss or avm/ptn/avd-lza/management-plane or avm/ptn/3-tier/web-app
  • Segments:
    • ptn defines this as a pattern module
    • <hyphenated grouping/category name> is a hierarchical grouping of pattern modules by category, with each word separated by dashes, such as:
      • project name, e.g., avd-lza,
      • primary resource provider, e.g., compute or network, or
      • architecture, e.g., 3-tier
    • <hyphenated pattern module name> is a term describing the module’s function, with each word separated by dashes, e.g., app-tier-vmss = Application Tier VMSS; management-plane = Azure Virtual Desktop Landing Zone Accelerator Management Plane

Terraform Pattern Module Naming

  • Naming convention:
    • avm-ptn-<pattern module name> (Module name for registry)
    • terraform-<provider>-avm-ptn-<pattern module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-ptn-apptiervmss or avm-ptn-avd-lza-managementplane
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • ptn defines this as a pattern module
    • <pattern module name> is a term describing the module’s function, e.g., apptiervmss = Application Tier VMSS; avd-lza-managementplane = Azure Virtual Desktop Landing Zone Accelerator Management Plane



See origin...

ID: PMNFR2 - Category: Composition - Use Resource Modules to Build a Pattern Module

A Pattern Module SHOULD be built from AVM Resources Modules to establish a standardized code base and improve maintainability. If a valid reason exists, a pattern module MAY contain native resources (“vanilla” code) where it’s necessary. A Pattern Module MUST NOT contain references to non-AVM modules.

Valid reasons for not using a Resource Module for a resource required by a Pattern Module include but are not limited to:

  • When using a Resource Module would result in hitting scaling limitations and/or would reduce the capabilities of the Pattern Module due to the limitations of Azure Resource Manager.
  • Developing a Pattern Module under time constraint, without having all required Resource Modules readily available.
Note

In the latter case, the Pattern Module SHOULD be updated to use the Resource Module when the required Resource Module becomes available, to avoid accumulating technical debt. Ideally, all required Resource Modules SHOULD be developed first, and then leveraged by the Pattern Module.




See origin...

ID: PMNFR3 - Category: Composition - Use other Pattern Modules to Build a Pattern Module

A Pattern Module MAY contain and be built using other AVM Pattern Modules. A Pattern Module MUST NOT contain references to non-AVM modules.




See origin...

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azurerm"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.




See origin...

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
azapi>= 2.0< 3.0
azurerm>= 4.0< 5.0
Note

Authors MAY select either Azurerm, Azapi, or both providers in their module.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.0"
    }
  }
}



See origin...

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1TFNFR6Resource & Data OrderSHOULDOwnerContributorBAU
2TFNFR7Count & for_each UseMUSTOwnerContributorBAU
3TFNFR8Resource & Data Block OrdersSHOULDOwnerContributorBAU
4TFNFR9Module Block OrderSHOULDOwnerContributorBAU
5TFNFR10No Double Quotes in ignore_changesMUSTOwnerContributorBAU
6TFNFR11Null Comparison ToggleSHOULDOwnerContributorBAU
7TFNFR12Dynamic for Optional Nested ObjectsMUSTOwnerContributorBAU
8TFNFR13Default Values with coalesce/trySHOULDOwnerContributorBAU
9TFNFR16Variable Naming RulesSHOULDOwnerContributorBAU
10TFNFR17Variables with DescriptionsSHOULDOwnerContributorBAU
11TFNFR18Variables with TypesMUSTOwnerContributorBAU
12TFNFR19Sensitive Data VariablesSHOULDOwnerContributorBAU
13TFNFR20Non-Nullable Defaults for collection valuesSHOULDOwnerContributorBAU
14TFNFR21Discourage Nullability by DefaultMUSTOwnerContributorBAU
15TFNFR22Avoid sensitive = falseMUSTOwnerContributorBAU
16TFNFR23Sensitive Default Value ConditionsMUSTOwnerContributorBAU
17TFNFR24Handling Deprecated VariablesMUSTOwnerContributorBAU
18TFNFR25Verified Modules RequirementsMUSTOwnerContributorBAU
19TFNFR26Providers in required_providersMUSTOwnerContributorBAU
20TFNFR27Provider Declarations in ModulesMUSTOwnerContributorBAU
22TFNFR30Handling Deprecated OutputsMUSTOwnerContributorBAU
23TFNFR31locals.tf for Locals OnlyMAYOwnerContributorBAU
25TFNFR33Precise Local TypesSHOULDOwnerContributorBAU
26TFNFR34Using Feature TogglesMUSTOwnerContributorBAU
27TFNFR35Reviewing Potential Breaking ChangesMUSTOwnerContributorBAU
28TFNFR36Setting prevent_deletion_if_contains_resourcesSHOULDOwnerContributorBAU
29TFNFR37Tool Usage by Module OwnerMAYOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: TFNFR6 - Category: Code Style - Resource & Data Order

For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.

Resources that have dependencies SHOULD be defined close to each other.




See origin...

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azurerm_network_security_group" "this" {
  count               = local.create_new_security_group ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azurerm_subnet.subnet0.name }`
  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"]
}

Bad example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_name_set // `set(string)`, when user use `toset([azurerm_subnet.subnet0.name])`, it would cause an error.
  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"]
}



See origin...

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azurerm_resource_group.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.




See origin...

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.




See origin...

ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes

The ignore_changes attribute MUST NOT be enclosed in double quotes.

Good example:

lifecycle {
    ignore_changes = [
      tags,
    ]
}

Bad example:

lifecycle {
    ignore_changes = [
      "tags",
    ]
}



See origin...

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azurerm_network_security_group" "this" {
  count               = var.security_group_id == null ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azurerm_network_security_group.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azurerm_network_security_group.foo.id
  }
}

This technique SHOULD be used under this use case only.




See origin...

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example from the community:

resource "azurerm_kubernetes_cluster" "main" {
  ...
  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type                      = var.identity_type
      user_assigned_identity_id = var.user_assigned_identity_id
    }
  }
  ...
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []



See origin...

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)



See origin...

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.




See origin...

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<-EOT
  - `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
  - `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
EOT
}



See origin...

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible, any MAY only be defined with adequate reasons.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)



See origin...

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.




See origin...

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.




See origin...

ID: TFNFR21 - Category: Code Style - Discourage Nullability by Default

nullable = true MUST be avoided.




See origin...

ID: TFNFR22 - Category: Code Style - Avoid sensitive = false

sensitive = false MUST be avoided.




See origin...

ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions

A default value MUST NOT be set for a sensitive input - e.g., a default password.




See origin...

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.




See origin...

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.11"
    }
  }
}



See origin...

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11.1, < 4.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11, < 4.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.11"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11"
    }
  }
}



See origin...

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
      configuration_aliases = [ azurerm.alternate ]
    }
  }
}

In the root module where we call this verified module:

provider "azurerm" {
  features {}
}

provider "azurerm" {
  alias = "alternate"
  features {}
}

module "foo" {
  source = "xxx"
  providers = {
    azurerm = azurerm
    azurerm.alternate = azurerm.alternate
  }
}

Bad example:

In verified module:

provider "azurerm" {
  # Configuration options
  features {}
}



See origin...

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.




See origin...

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.




See origin...

ID: TFNFR33 - Category: Code Style - Precise Local Types

Precise local types SHOULD be used.

Good example:

{
  name = "John"
  age  = 52
}

Bad example:

{
  name = "John"
  age  = "52" # age should be number
}



See origin...

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azurerm_route_table" "this" {
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azurerm_route_table" "this" {
  count               = var.create_route_table ? 1 : 0
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}



See origin...

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.




See origin...

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.




See origin...

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.




Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4PMNFR5Parameter/Variable NamingSHOULDOwnerContributorBAU
5TFFR2Additional Terraform OutputsSHOULDOwnerContributorBAU
6TFNFR14Not allowed variablesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR14 - Category: Inputs - Data Types

A module SHOULD use either: simple data types. e.g., string, int, bool.

OR

Complex data types (objects, arrays, maps) when the language-compliant schema is defined.




See origin...

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: PMNFR5 - Category: Inputs - Parameter/Variable Naming

Parameter/variable input names SHOULD contain the resource to which they pertain. E.g., virtualMachineSku/virtualmachine_sku




See origin...

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azurerm_resource_mychildresource : key => value.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.




Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9TFNFR5Test ToolingMUSTOwnerContributorBAU
10TFNFR15Variable Definition OrderSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).




See origin...

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.




See origin...

ID: SNFR5 - Category: Testing - Upgrade Tests

Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.




See origin...

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.




See origin...

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.




See origin...

ID: TFNFR15 - Category: Code Style - Variable Definition Order

Input variables SHOULD follow this order:

  1. All required fields, in alphabetical order
  2. All optional fields, in alphabetical order

A variable without default value is a required field, otherwise it’s an optional one.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3TFNFR1DescriptionsMUSTOwnerContributorBAU
4TFNFR2Module Documentation GenerationMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR15 - Category: Documentation - Automatic Documentation Generation

README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  



See origin...

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  



Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.




Terraform Resource Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8PMNFR4Missing Resource Module(s)MUSTOwnerContributorBAU
9TFNFR3GitHub Repo Branch ProtectionMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

ID: SNFR11 - Category: Contribution/Support - Issues Response Times

A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.




See origin...

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.




See origin...

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: PMNFR4 - Category: Hygiene - Missing Resource Module(s)

An item MUST be logged onto as an issue on the AVM Central Repo (Azure/Azure-Verified-Modules) if a Resource Module does not exist for resources deployed by the pattern module.

Exception

If the Resource Module adds no value, see Resource Module functional requirement ID: RMFR2.




See origin...

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.




Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
βž• See Specifications for this category
See origin...

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}



See origin...

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.




Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6RMFR1Single Resource OnlyMUSTOwnerContributorBAU
7RMFR2No Resource Wrapper ModulesMUSTOwnerInitial
8RMFR3Resource GroupsMUSTOwnerContributorBAU
9RMFR4AVM Consistent Feature & Extension Resources Value AddMUSTOwnerContributorBAU
10RMFR5AVM Consistent Feature & Extension Resources Value Add Interfaces/SchemasMUSTOwnerContributorBAU
11RMFR8Dependency on child and other resourcesMUSTOwnerContributorBAU
12RMFR9End-of-life resource versionsSHOULDOwnerContributorBAU
13RMNFR1Module NamingMUSTOwnerInitial
14RMNFR3RP CollaborationSHOULDOwnerBAU
15TFFR1Cross-Referencing ModulesMUSTOwnerContributorBAU
16TFFR3Providers - Permitted VersionsMUSTOwnerContributorBAU
17TFNFR4Lower snake_casingMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”



See origin...

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.




See origin...

ID: RMFR1 - Category: Composition - Single Resource Only

A resource module MUST only deploy a single instance of the primary resource, e.g., one virtual machine per instance.

Multiple instances of the module MUST be used to scale out.




See origin...

ID: RMFR2 - Category: Composition - No Resource Wrapper Modules

A resource module MUST add value by including additional features on top of the primary resource.




See origin...

ID: RMFR3 - Category: Composition - Resource Groups

A resource module MUST NOT create a Resource Group for resources that require them.

In the case that a Resource Group is required, a module MUST have an input (scope or variable):

  • In Bicep the targetScope MUST be set to resourceGroup or not specified (which means default to resourceGroup scope)
  • In Terraform the variable MUST be called resource_group_name

Scopes will be covered further in the respective language specific specifications.




See origin...

ID: RMFR4 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add

Resource modules support the following optional features/extension resources, as specified, if supported by the primary resource. The top-level variable/parameter names MUST be:

Optional Features/Extension ResourcesBicep Parameter NameTerraform Variable NameMUST/SHOULD
Diagnostic SettingsdiagnosticSettingsdiagnostic_settingsMUST
Role AssignmentsroleAssignmentsrole_assignmentsMUST
Resource LockslocklockMUST
TagstagstagsMUST
Managed Identities (System / User Assigned)managedIdentitiesmanaged_identitiesMUST
Private EndpointsprivateEndpointsprivate_endpointsMUST
Customer Managed KeyscustomerManagedKeycustomer_managed_keyMUST
Azure Monitor AlertsalertsalertsSHOULD

Resource modules MUST NOT deploy required/dependent resources for the optional features/extension resources specified above. For example, for Diagnostic Settings the resource module MUST NOT deploy the Log Analytics Workspace, this is expected to be already in existence from the perspective of the resource module deployed via another method/module etc.

Note

Please note that the implementation of Customer Managed Keys from an ARM API perspective is different across various RPs that implement Customer Managed Keys in their service. For that reason you may see differences between modules on how Customer Managed Keys are handled and implemented, but functionality will be as expected.

Module owners MAY choose to utilize cross repo dependencies for these “add-on” resources, or MAY chose to implement the code directly in their own repo/module. So long as the implementation and outputs are as per the specifications requirements, then this is acceptable.

Tip

Make sure to checkout the language specific specifications for more info on this:




See origin...

ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas

Resource modules MUST implement a common interface, e.g. the input’s data structures and properties within them (objects/arrays/dictionaries/maps), for the optional features/extension resources:

See:




See origin...

ID: RMFR8 - Category: Composition - Dependency on child and other resources

A resource module MAY contain references to other resource modules, however MUST NOT contain references to non-AVM modules nor AVM pattern modules.

See BCPFR1 and TFFR1 for more information on this.




See origin...

ID: RMFR9 - Category: Composition - End-of-life resource versions

When a given version of an Azure resource used in a resource module reaches its end-of-life (EOL) and is no longer supported by Microsoft, the module owner SHOULD ensure that:

  1. The module is aligned with these changes and only includes supported versions of the resource. This is typically achieved through the allowed values in the parameter that specifies the resource SKU or type.
  2. The following notice is shown under the Notes section of the module’s readme.md. (If any related public announcement is available, it can also be linked to from the Notes section.):

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from the related parameters.”

  3. AND the related parameter’s description:

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from this parameter.”




See origin...

ID: RMNFR1 - Category: Naming - Module Naming

Resource modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Note

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the correct singular names for all resource types to enable checks to utilize this list to ensure repos are named correctly. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

This will be updated quarterly, or ad-hoc as new RPs/ Resources are created and highlighted via a check failure.

Bicep Resource Module Naming

  • Naming convention (module name for registry): avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>
  • Example: avm/res/compute/virtual-machine or avm/res/managed-identity/user-assigned-identity
  • Segments:
    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute = compute, Microsoft.ManagedIdentity = managed-identity.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute/virtualMachines = virtual-machine, BUT Microsoft.Network/trafficmanagerprofiles = trafficmanagerprofile - since trafficmanagerprofiles is all lower case as per the ARM API definition.

Bicep Child Module Naming

  • Naming convention (module name for registry):avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>/ <hyphenated child resource type/<hyphenated grandchild resource type>/<etc.>

  • Example: avm/res/network/virtual-network/subnet or avm/res/storage/storage-account/blob-service/container

  • Segments:

    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network = network.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks = virtual-network.
    • <hyphenated child resource type (to be repeated for grandchildren, etc.)> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks/subnets = subnet or Microsoft.Storage/storageAccounts/blobServices/containers = blob-service/container.

Terraform Resource Module Naming

  • Naming convention:
    • avm-res-<resource provider>-<ARM resource type> (module name for registry)
    • terraform-<provider>-avm-res-<resource provider>-<ARM resource type> (GitHub repository name to meet registry naming requirements)
  • Example: avm-res-compute-virtualmachine or avm-res-managedidentity-userassignedidentity
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • res defines this is a resource module
    • <resource provider> is the resource provider’s name after the Microsoft part, e.g., Microsoft.Compute = compute.
    • <ARM resource type> is the singular version of the word after the resource provider, e.g., Microsoft.Compute/virtualMachines = virtualmachine



See origin...

ID: RMNFR3 - Category: Composition - RP Collaboration

Module owners (Microsoft FTEs) SHOULD reach out to the respective Resource Provider teams to build a partnership and collaboration on the modules creation, existence and long term maintenance.

Review this wiki page (Microsoft Internal) for more information.




See origin...

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azurerm"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.




See origin...

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
azapi>= 2.0< 3.0
azurerm>= 4.0< 5.0
Note

Authors MAY select either Azurerm, Azapi, or both providers in their module.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.0"
    }
  }
}



See origin...

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1TFNFR6Resource & Data OrderSHOULDOwnerContributorBAU
2TFNFR7Count & for_each UseMUSTOwnerContributorBAU
3TFNFR8Resource & Data Block OrdersSHOULDOwnerContributorBAU
4TFNFR9Module Block OrderSHOULDOwnerContributorBAU
5TFNFR10No Double Quotes in ignore_changesMUSTOwnerContributorBAU
6TFNFR11Null Comparison ToggleSHOULDOwnerContributorBAU
7TFNFR12Dynamic for Optional Nested ObjectsMUSTOwnerContributorBAU
8TFNFR13Default Values with coalesce/trySHOULDOwnerContributorBAU
9TFNFR16Variable Naming RulesSHOULDOwnerContributorBAU
10TFNFR17Variables with DescriptionsSHOULDOwnerContributorBAU
11TFNFR18Variables with TypesMUSTOwnerContributorBAU
12TFNFR19Sensitive Data VariablesSHOULDOwnerContributorBAU
13TFNFR20Non-Nullable Defaults for collection valuesSHOULDOwnerContributorBAU
14TFNFR21Discourage Nullability by DefaultMUSTOwnerContributorBAU
15TFNFR22Avoid sensitive = falseMUSTOwnerContributorBAU
16TFNFR23Sensitive Default Value ConditionsMUSTOwnerContributorBAU
17TFNFR24Handling Deprecated VariablesMUSTOwnerContributorBAU
18TFNFR25Verified Modules RequirementsMUSTOwnerContributorBAU
19TFNFR26Providers in required_providersMUSTOwnerContributorBAU
20TFNFR27Provider Declarations in ModulesMUSTOwnerContributorBAU
22TFNFR30Handling Deprecated OutputsMUSTOwnerContributorBAU
23TFNFR31locals.tf for Locals OnlyMAYOwnerContributorBAU
25TFNFR33Precise Local TypesSHOULDOwnerContributorBAU
26TFNFR34Using Feature TogglesMUSTOwnerContributorBAU
27TFNFR35Reviewing Potential Breaking ChangesMUSTOwnerContributorBAU
28TFNFR36Setting prevent_deletion_if_contains_resourcesSHOULDOwnerContributorBAU
29TFNFR37Tool Usage by Module OwnerMAYOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: TFNFR6 - Category: Code Style - Resource & Data Order

For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.

Resources that have dependencies SHOULD be defined close to each other.




See origin...

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azurerm_network_security_group" "this" {
  count               = local.create_new_security_group ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azurerm_subnet.subnet0.name }`
  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"]
}

Bad example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_name_set // `set(string)`, when user use `toset([azurerm_subnet.subnet0.name])`, it would cause an error.
  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"]
}



See origin...

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azurerm_resource_group.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.




See origin...

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.




See origin...

ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes

The ignore_changes attribute MUST NOT be enclosed in double quotes.

Good example:

lifecycle {
    ignore_changes = [
      tags,
    ]
}

Bad example:

lifecycle {
    ignore_changes = [
      "tags",
    ]
}



See origin...

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azurerm_network_security_group" "this" {
  count               = var.security_group_id == null ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azurerm_network_security_group.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azurerm_network_security_group.foo.id
  }
}

This technique SHOULD be used under this use case only.




See origin...

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example from the community:

resource "azurerm_kubernetes_cluster" "main" {
  ...
  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type                      = var.identity_type
      user_assigned_identity_id = var.user_assigned_identity_id
    }
  }
  ...
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []



See origin...

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)



See origin...

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.




See origin...

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<-EOT
  - `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
  - `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
EOT
}



See origin...

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible, any MAY only be defined with adequate reasons.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)



See origin...

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.




See origin...

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.




See origin...

ID: TFNFR21 - Category: Code Style - Discourage Nullability by Default

nullable = true MUST be avoided.




See origin...

ID: TFNFR22 - Category: Code Style - Avoid sensitive = false

sensitive = false MUST be avoided.




See origin...

ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions

A default value MUST NOT be set for a sensitive input - e.g., a default password.




See origin...

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.




See origin...

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.11"
    }
  }
}



See origin...

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11.1, < 4.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11, < 4.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.11"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11"
    }
  }
}



See origin...

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
      configuration_aliases = [ azurerm.alternate ]
    }
  }
}

In the root module where we call this verified module:

provider "azurerm" {
  features {}
}

provider "azurerm" {
  alias = "alternate"
  features {}
}

module "foo" {
  source = "xxx"
  providers = {
    azurerm = azurerm
    azurerm.alternate = azurerm.alternate
  }
}

Bad example:

In verified module:

provider "azurerm" {
  # Configuration options
  features {}
}



See origin...

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.




See origin...

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.




See origin...

ID: TFNFR33 - Category: Code Style - Precise Local Types

Precise local types SHOULD be used.

Good example:

{
  name = "John"
  age  = 52
}

Bad example:

{
  name = "John"
  age  = "52" # age should be number
}



See origin...

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azurerm_route_table" "this" {
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azurerm_route_table" "this" {
  count               = var.create_route_table ? 1 : 0
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}



See origin...

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.




See origin...

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.




See origin...

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.




Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4RMFR6Parameter/Variable NamingMUSTOwnerContributorBAU
5RMFR7Minimum Required OutputsMUSTOwnerContributorBAU
6RMNFR2Parameter/Variable NamingMUSTOwnerContributorBAU
7TFFR2Additional Terraform OutputsSHOULDOwnerContributorBAU
8TFNFR14Not allowed variablesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR14 - Category: Inputs - Data Types

A module SHOULD use either: simple data types. e.g., string, int, bool.

OR

Complex data types (objects, arrays, maps) when the language-compliant schema is defined.




See origin...

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: RMFR6 - Category: Inputs - Parameter/Variable Naming

Parameters/variables that pertain to the primary resource MUST NOT use the resource type in the name.

e.g., use sku, vs. virtualMachineSku/virtualmachine_sku

Another example for where RPs contain some of their name within a property, leave the property unchanged. E.g. Key Vault has a property called keySize, it is fine to leave as this and not remove the key part from the property/parameter name.




See origin...

ID: RMFR7 - Category: Outputs - Minimum Required Outputs

Module owners MUST output the following outputs as a minimum in their modules:

OutputBicep Output NameTerraform Output Name
Resource Namenamename
Resource IDresourceIdresource_id
System Assigned Managed Identity Principal ID (if supported by module)systemAssignedMIPrincipalIdsystem_assigned_mi_principal_id
Tip

Module owners MAY also have to provide additional outputs depending on the IaC language, please check the language specific specs:




See origin...

ID: RMNFR2 - Category: Inputs - Parameter/Variable Naming

A resource module MUST use the following standard inputs:

  • name (no default)
  • location (if supported by the resource and not a global resource, then use Resource Group location, if resource supports Resource Groups, otherwise no default)



See origin...

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azurerm_resource_mychildresource : key => value.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.




Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9TFNFR5Test ToolingMUSTOwnerContributorBAU
10TFNFR15Variable Definition OrderSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).




See origin...

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.




See origin...

ID: SNFR5 - Category: Testing - Upgrade Tests

Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.




See origin...

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.




See origin...

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.




See origin...

ID: TFNFR15 - Category: Code Style - Variable Definition Order

Input variables SHOULD follow this order:

  1. All required fields, in alphabetical order
  2. All optional fields, in alphabetical order

A variable without default value is a required field, otherwise it’s an optional one.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3TFNFR1DescriptionsMUSTOwnerContributorBAU
4TFNFR2Module Documentation GenerationMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR15 - Category: Documentation - Automatic Documentation Generation

README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  



See origin...

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  



Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.




Terraform Utility Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8TFNFR3GitHub Repo Branch ProtectionMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

ID: SNFR11 - Category: Contribution/Support - Issues Response Times

A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.




See origin...

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.




See origin...

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.




Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
βž• See Specifications for this category
See origin...

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}



See origin...

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.




Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SNFR25Resource NamingMUSTOwnerInitial
4UMNFR1Module NamingMUSTOwnerInitial
5TFFR1Cross-Referencing ModulesMUSTOwnerContributorBAU
6TFFR3Providers - Permitted VersionsMUSTOwnerContributorBAU
7TFNFR4Lower snake_casingMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”



See origin...

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.




See origin...

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.




See origin...

ID: UMNFR1 - Category: Naming - Module Naming

Utility Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Utility Module Naming

  • Naming convention: avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name>
  • Example: avm/utl/general/get-environment or avm/utl/types/avm-common-types
  • Segments:
    • utl defines this as a utility module
    • <hyphenated grouping/category name> is a hierarchical grouping of utility modules by category, with each word separated by dashes, such as: general or types
    • <hyphenated utility module name> is a term describing the module’s function, with each word separated by dashes, e.g., get-environment = to get environmental details; avm-common-types = to use common types.

Terraform Utility Module Naming

  • Naming convention:
    • avm-utl-<utility module name> (Module name for registry)
    • terraform-<provider>-avm-utl-<utility module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-utl-sku-finder or avm-utl-naming
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • utl defines this as a utility module
    • <utility module name> is a term describing the module’s function, e.g., sku-finder = to find available SKUs; naming = to handle naming conventions.



See origin...

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azurerm"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.




See origin...

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
azapi>= 2.0< 3.0
azurerm>= 4.0< 5.0
Note

Authors MAY select either Azurerm, Azapi, or both providers in their module.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.0"
    }
  }
}



See origin...

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1TFNFR6Resource & Data OrderSHOULDOwnerContributorBAU
2TFNFR7Count & for_each UseMUSTOwnerContributorBAU
3TFNFR8Resource & Data Block OrdersSHOULDOwnerContributorBAU
4TFNFR9Module Block OrderSHOULDOwnerContributorBAU
5TFNFR10No Double Quotes in ignore_changesMUSTOwnerContributorBAU
6TFNFR11Null Comparison ToggleSHOULDOwnerContributorBAU
7TFNFR12Dynamic for Optional Nested ObjectsMUSTOwnerContributorBAU
8TFNFR13Default Values with coalesce/trySHOULDOwnerContributorBAU
9TFNFR16Variable Naming RulesSHOULDOwnerContributorBAU
10TFNFR17Variables with DescriptionsSHOULDOwnerContributorBAU
11TFNFR18Variables with TypesMUSTOwnerContributorBAU
12TFNFR19Sensitive Data VariablesSHOULDOwnerContributorBAU
13TFNFR20Non-Nullable Defaults for collection valuesSHOULDOwnerContributorBAU
14TFNFR21Discourage Nullability by DefaultMUSTOwnerContributorBAU
15TFNFR22Avoid sensitive = falseMUSTOwnerContributorBAU
16TFNFR23Sensitive Default Value ConditionsMUSTOwnerContributorBAU
17TFNFR24Handling Deprecated VariablesMUSTOwnerContributorBAU
18TFNFR25Verified Modules RequirementsMUSTOwnerContributorBAU
19TFNFR26Providers in required_providersMUSTOwnerContributorBAU
20TFNFR27Provider Declarations in ModulesMUSTOwnerContributorBAU
22TFNFR30Handling Deprecated OutputsMUSTOwnerContributorBAU
23TFNFR31locals.tf for Locals OnlyMAYOwnerContributorBAU
25TFNFR33Precise Local TypesSHOULDOwnerContributorBAU
26TFNFR34Using Feature TogglesMUSTOwnerContributorBAU
27TFNFR35Reviewing Potential Breaking ChangesMUSTOwnerContributorBAU
28TFNFR36Setting prevent_deletion_if_contains_resourcesSHOULDOwnerContributorBAU
29TFNFR37Tool Usage by Module OwnerMAYOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: TFNFR6 - Category: Code Style - Resource & Data Order

For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.

Resources that have dependencies SHOULD be defined close to each other.




See origin...

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azurerm_network_security_group" "this" {
  count               = local.create_new_security_group ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azurerm_subnet.subnet0.name }`
  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"]
}

Bad example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_name_set // `set(string)`, when user use `toset([azurerm_subnet.subnet0.name])`, it would cause an error.
  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"]
}



See origin...

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azurerm_resource_group.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.




See origin...

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.




See origin...

ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes

The ignore_changes attribute MUST NOT be enclosed in double quotes.

Good example:

lifecycle {
    ignore_changes = [
      tags,
    ]
}

Bad example:

lifecycle {
    ignore_changes = [
      "tags",
    ]
}



See origin...

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azurerm_network_security_group" "this" {
  count               = var.security_group_id == null ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azurerm_network_security_group.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azurerm_network_security_group.foo.id
  }
}

This technique SHOULD be used under this use case only.




See origin...

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example from the community:

resource "azurerm_kubernetes_cluster" "main" {
  ...
  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type                      = var.identity_type
      user_assigned_identity_id = var.user_assigned_identity_id
    }
  }
  ...
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []



See origin...

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)



See origin...

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.




See origin...

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<-EOT
  - `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
  - `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
EOT
}



See origin...

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible, any MAY only be defined with adequate reasons.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)



See origin...

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.




See origin...

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.




See origin...

ID: TFNFR21 - Category: Code Style - Discourage Nullability by Default

nullable = true MUST be avoided.




See origin...

ID: TFNFR22 - Category: Code Style - Avoid sensitive = false

sensitive = false MUST be avoided.




See origin...

ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions

A default value MUST NOT be set for a sensitive input - e.g., a default password.




See origin...

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.




See origin...

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.11"
    }
  }
}



See origin...

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11.1, < 4.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11, < 4.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.11"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11"
    }
  }
}



See origin...

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
      configuration_aliases = [ azurerm.alternate ]
    }
  }
}

In the root module where we call this verified module:

provider "azurerm" {
  features {}
}

provider "azurerm" {
  alias = "alternate"
  features {}
}

module "foo" {
  source = "xxx"
  providers = {
    azurerm = azurerm
    azurerm.alternate = azurerm.alternate
  }
}

Bad example:

In verified module:

provider "azurerm" {
  # Configuration options
  features {}
}



See origin...

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.




See origin...

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.




See origin...

ID: TFNFR33 - Category: Code Style - Precise Local Types

Precise local types SHOULD be used.

Good example:

{
  name = "John"
  age  = 52
}

Bad example:

{
  name = "John"
  age  = "52" # age should be number
}



See origin...

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azurerm_route_table" "this" {
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azurerm_route_table" "this" {
  count               = var.create_route_table ? 1 : 0
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}



See origin...

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.




See origin...

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.




See origin...

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.




Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4TFFR2Additional Terraform OutputsSHOULDOwnerContributorBAU
5TFNFR14Not allowed variablesMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR14 - Category: Inputs - Data Types

A module SHOULD use either: simple data types. e.g., string, int, bool.

OR

Complex data types (objects, arrays, maps) when the language-compliant schema is defined.




See origin...

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azurerm_resource_mychildresource : key => value.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}



See origin...

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.




Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8TFNFR5Test ToolingMUSTOwnerContributorBAU
9TFNFR15Variable Definition OrderSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).




See origin...

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.




See origin...

ID: SNFR5 - Category: Testing - Upgrade Tests

Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.




See origin...

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.




See origin...

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.




See origin...

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.




See origin...

ID: TFNFR15 - Category: Code Style - Variable Definition Order

Input variables SHOULD follow this order:

  1. All required fields, in alphabetical order
  2. All optional fields, in alphabetical order

A variable without default value is a required field, otherwise it’s an optional one.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3TFNFR1DescriptionsMUSTOwnerContributorBAU
4TFNFR2Module Documentation GenerationMUSTOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR15 - Category: Documentation - Automatic Documentation Generation

README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  



See origin...

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  



Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
βž• See Specifications for this category
See origin...

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.




BCPFR1 - Cross-Referencing Modules

ID: BCPFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-reference other modules to build either Resource or Pattern modules.

However, they MUST be referenced only by a public registry reference to a pinned version e.g. br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<. They MUST NOT use local parent path references to a module e.g. ../../xxx/yyy.bicep.

The only exception to this rule are child modules as documented in BCPFR6.

Modules MUST NOT contain references to non-AVM modules.

BCPFR2 - Role Assignments Role Definition Mapping

ID: BCPFR2 - Category: Composition - Role Assignments Role Definition Mapping

Module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID, this should be self contained within the module themselves.

However, they MUST use only the official RBAC Role Definition name within the variable and nothing else.

To meet the requirements of BCPFR2, BCPNFR5 and BCPNFR6 you MUST use the below code sample in your AVM Modules to achieve this.

  @description('''Required. You can provide either the display name (note not all roles are supported, check module documentation) of the role definition, or its fully qualified ID in the following format: `/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11`.''')
  param roleDefinitionIdOrName string
  
  var builtInRbacRoleNames = {
    Owner: '/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
    Contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    Reader: '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
    'Role Based Access Control Administrator (Preview)': '/providers/Microsoft.Authorization/roleDefinitions/f58310d9-a9f6-439a-9e8d-f62e7b41a168'
    'User Access Administrator': '/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
    //Other RBAC Role Definitions Names & IDs can be added here as needed for your module
  }
  
  var roleDefinitionIdMappedResult = (contains(builtInRbacRoleNames, roleDefinitionIdOrName) ? builtInRbacRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName)
  
  resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
    //Other properties removed for ease of reading
    properties: {
      roleDefinitionId: roleDefinitionIdMappedResult
      //Other properties removed for ease of reading
    }
  }
  

BCPFR4 - Telemetry Enablement

ID: BCPFR4 - Category: Composition - Telemetry Enablement

To comply with specifications outlined in SFR3 & SFR4 you MUST incorporate the following code snippet into your modules. Place this code sample in the “top level” main.bicep file; it is not necessary to include it in any nested Bicep files (child modules), unless they are marked for direct publishing (Ref Child module publishing).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: []
      outputs: {
        telemetry: {
          type: 'String'
          value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
        }
      }
    }
  }
}

BCPFR6 - Cross-Referencing Child-Modules

ID: BCPFR6 - Cross-Referencing Child-Modules

Parent templates MUST reference all their direct child-templates to allow for an end-to-end deployment experience.
For example, the SQL server template must reference its child database module and encapsulate it in a loop to allow for the deployment of multiple databases.

@description('Optional. The databases to create in the server')
param databases databaseType[]?

resource server 'Microsoft.Sql/servers@(...)' = { (...) }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]

BCPFR7 - Cross-Referencing Modules

ID: BCPFR7 - Cross-Referencing published Modules

Resource modules, that reference other modules (child, utility, or other resource modules), MUST disable the telemetry on the referenced modules.

Note

This only applies to resource modules that reference other modules, such as:

  • other resource modules
  • utility modules
  • child-modules qualifying for publishing, i.e. having a version.json file in their directory and exposing the enableTelemetry input parameter

For pattern modules, SFR4 still applies.

A variable named enableReferencedModulesTelemetry is created in the main.bicep file of the module, that cross-references other published modules, and set to false. This variable is used to set the enableTelemetry parameter of cross-referenced modules.

var enableReferencedModulesTelemetry = false

// local referencing
module virtualNetwork_subnets 'subnet/main.bicep' = [
  for (subnet, index) in (subnets ?? []): {
    name: '${uniqueString(virtualNetwork.id, location)}-subnet-${index}'
    params: {
      (...)
      enableTelemetry: enableReferencedModulesTelemetry
    }
  }
]

// published module reference
module virtualNetwork_subnet 'br/public:avm/res/network/virtual-network/subnet:0.1.0' = {
  name: '${uniqueString(virtualNetwork.id, location)}-subnet-${index}'
    params: {
      (...)
      enableTelemetry: enableReferencedModulesTelemetry
    }
}

BCPNFR1 - Complex data types - General

ID: BCPNFR1 - Category: Inputs - Complex data types - General

To simplify the consumption experience for module consumers when interacting with complex data types input parameters, mainly objects and arrays, the Bicep features of Resource-Derived Types or User-Defined Types MUST be used and declared.

Tip

User-Defined Types are GA in Bicep as of version v0.21.1, Resource-Derived Types are GA as of version v0.34.1, please ensure you have this version(s) installed as a minimum.

Resource-Derived Types and User-Defined Types allow intellisense support in supported IDEs (e.g. Visual Studio Code) for complex input parameters using objects and array of objects.

v0.x Exemption

While we allow the release of major versions, starting with v1.0.0, retrofitting Resource-Derived Types and User-Defined Types for all modules will take a considerable amount of time.

Therefore, the addition of these features is currently NOT mandated/enforced. However, all modules MUST implement Resource-Derived Types and User-Defined Types prior to the release of their v1.0.0 version.

BCPNFR11 - Test Tooling

ID: BCPNFR11 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.

  • PSRule for Azure
  • Pester
    • Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Pester for your own tests.

BCPNFR12 - Deployment Test Naming

ID: BCPNFR12 - Category: Testing - Deployment Test Naming

Module owners MUST invoke the module in their test using the syntax:

module testDeployment '../../../main.bicep' =

Example 1: Working example with a single deployment

module testDeployment '../../../main.bicep' = {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}'
  params: {
    (...)
  }
}

Example 2: Working example using a deployment loop

@batchSize(1)
module testDeployment '../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {
  scope: resourceGroup
  name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}'
  params: {
    (...)
  }
}]

The syntax is used by the ReadMe-generating utility to identify, pull & format usage examples.

BCPNFR13 - Test file metadata

ID: BCPNFR13 - Category: Testing - Test file metadata

By default, the ReadMe-generating utility will create usage examples headers based on each e2e folder’s name.
Module owners MAY provide a custom name & description by specifying the metadata blocks name & description in their main.test.bicep test files.

For example:

metadata name = 'Using Customer-Managed-Keys with System-Assigned identity'
metadata description = 'This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.'

would lead to a header in the module’s readme.md file along the lines of

### Example 1: _Using Customer-Managed-Keys with System-Assigned identity_

This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.

BCPNFR14 - Versioning

ID: BCPNFR14 - Category: Composition - Versioning

To meet SNFR17 and depending on the changes you make, you may need to bump the version in the version.json file.

  {
    "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
    "version": "0.1"
  }
  

The version value is in the form of MAJOR.MINOR. The PATCH version will be incremented by the CI automatically when publishing the module to the Public Bicep Registry once the corresponding pull request is merged. Therefore, contributions that would only require an update of the patch version, can keep the version.json file intact.

For example, the version value should be:

  • 0.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.

BCPNFR15 - AVM Module Issue template file

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...

BCPNFR16 - Post-deployment tests

ID: BCPNFR16 - Category: Testing - Post-deployment tests

For each test case in the e2e folder, you can optionally add post-deployment Pester tests that are executed once the corresponding deployment completed and before the removal logic kicks in.

To leverage the feature you MUST:

  • Use Pester as a test framework in each test file

  • Name the file with the suffix "*.tests.ps1"

  • Place each test file the e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file returns, as well as the path to the test template file in case you want to extract data from it directly.

    For example, with an output such as output resourceId string = testDeployment[1].outputs.resourceId defined in the main.test.bicep file, the $TestInputData would look like:

    $TestInputData = @{
      DeploymentOutputs    = @{
        resourceId = @{
          Type  = "String"
          Value = "/subscriptions/***/resourceGroups/dep-***-keyvault.vaults-kvvpe-rg/providers/Microsoft.KeyVault/vaults/***kvvpe001"
        }
      }
      ModuleTestFolderPath = "/home/runner/work/bicep-registry-modules/bicep-registry-modules/avm/res/key-vault/vault/tests/e2e/private-endpoint"
    }

    A full test file may look like:

    βž• Pester post-deployment test file example
    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )
    
    Describe 'Validate private endpoint deployment' {
    
        Context 'Validate sucessful deployment' {
    
            It "Private endpoints should be deployed in resource group" {
    
                $keyVaultResourceId = $TestInputData.DeploymentOutputs.resourceId.Value
                $testResourceGroup = ($keyVaultResourceId -split '\/')[4]
                $deployedPrivateEndpoints = Get-AzPrivateEndpoint -ResourceGroupName $testResourceGroup
                $deployedPrivateEndpoints.Count | Should -BeGreaterThan 0
            }
        }
    }

BCPNFR17 - Code Styling - Type casting

ID: BCPNFR17 - Category: Composition - Code Styling - Type casting

To improve the usability of primitive module properties declared as strings, you SHOULD declare them using a type which better represents them, and apply any required casting in the module on behalf of the user.

For reference, please refer to the following examples:

Boolean as String

Boolean as String
@allowed([
  'false'
  'true'
])
param myParameterValue string = 'false'

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: myParameterValue
  }
}
param myParameterValue string = false

resource myResource '(...)' = {
  (...)
  properties: {
    myParameter: string(myParameterValue)
  }
}

Integer Array as String Array

Integer Array as String Array
@allowed([
  '1'
  '2'
  '3'
])
param zones array

resource myResource '(...)' = {
  (...)
  properties: {
    zones: zones
  }
}
@allowed([
  1
  2
  3
])
param zones int[]

resource myResource '(...)' = {
  (...)
  properties: {
    zones: map(zones, zone => string(zone))
  }
}

BCPNFR18 - User-defined types - Specification

ID: BCPNFR18 - User-defined types - Specification

User-defined types (UDTs) MUST always be singular and non-nullable. The configuration of either should instead be done directly at the parameter or output that uses the type.

For example, instead of

param subnets subnetsType
type subnetsType = { ... }[]?

the type should be defined like

param subnets subnetType[]?
type subnetType = { ... }

The primary reason for this requirement is clarity. If not defined directly at the parameter or output, a user would always be required to check the type to understand how e.g., a parameter is expected.

BCPNFR19 - User-defined types - Naming

ID: BCPNFR19 - User-defined types - Naming

User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.

type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs

Since User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.

type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct

BCPNFR2 - Module Documentation Generation

ID: BCPNFR2 - Category: Documentation - Module Documentation Generation

Note

This script/tool is currently being developed by the AVM team and will be made available very soon.

Bicep modules documentation MUST be automatically generated via the provided script/tooling from the AVM team, providing the following headings:

  • Title
  • Description
  • Navigation
  • Resource Types
  • Usage Examples
  • Parameters
  • Outputs
  • Cross-referenced modules

BCPNFR20 - User-defined types - Export

ID: BCPNFR20 - User-defined types - Export

User-defined types (UDTs) SHOULD always be exported via the @export() annotation in every template they’re implemented in.

@export()
type subnetType = { ... }

Doing so has the benefit that other (e.g., parent) modules can import them and as such reduce code duplication. Also, if the module itself is published, users of the Public Bicep Registry can import the types independently of the module itself. One example where this can be useful is a pattern module that may re-use the same interface when referencing a module from the registry.

BCPNFR21 - User-defined types - Decorators

ID: BCPNFR21 - User-defined types - Decorators

Similar to BCPNFR9, User-defined types (UDTs) MUST implement decorators such as description & secure (if sensitive). This is true for every property of the UDT, as well as the UDT itself.

Further, User-defined types SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('My type''s description.')
type myType = {
  @description('Optional. The threshold of your resource.')
  @minValue(1)
  @maxValue(10)
  threshold: int?

  @description('Required. The SKU of your resource.')
  sku: ('Basic' | 'Premium' | 'Standard')
}

BCPNFR22 - Bicep Module Changelog

ID: BCPNFR22 - Category: Publishing - Changelog

When a module to be published (i.e., that has a version.json file) is changed, an entry MUST be created in the CHANGELOG.md file in the module folder. A link to the latest version of the changelog file has to be included at the top of the file, just below the # Changelog line. It is surrounded by empty lines.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/<ptn|res|utl>/<namespace/modulename[/submodulePath]>/CHANGELOG.md).

For each new version, an entry MUST be created above all existing versions in the CHANGELOG.md file of the module.

## <version>

### Changes

- This changed
- And this also

### Breaking Changes

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may be added as content for a section.
  • MUST exist only once.
  • All versions appear in descending order, which puts the most recent changes at the top.

What SHOULD be listed in the (Breaking) Changes section:

  • Relevant changes for the module
  • Changes in tests do not need to be added
Note

The versioning is following the SNFR17 - Semantic Versioning spec.

Example content of the CHANGELOG.md

A CHANGELOG.md file in the module’s root folder MUST start with the # Changelog header, followed by an empty line and a link to the latest published version of the changelog file, followed by another empty line. A section for each published version follows. Newer versions are placed above older versions.

# Changelog

The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/res/aad/domain-service/CHANGELOG.md).

## 0.2.1

### Changes

- Updated the referenced AVM common types

### Breaking Changes

- None

## 0.2.0

### Changes

- Implemented the minCPU parameter
- Updated the referenced VirtualNetwork module
- Updated the referenced AVM common types

### Breaking Changes

- The minCPU parameter is mandatory

## 0.1.0

### Changes

- Initial Release

### Breaking Changes

- None

Each bullet point should start with a capital letter.

Manual Editing

It is possible to modify the changelog content any time, e.g., to add missing versions, which will not create a new release of the module itself. Please note the following requirements in all cases:

  • All versions in the file, need to be valid and available as published version
  • Every version needs the two sections ## Changes and ## Breaking Changes with content
Note

Azure Verified Modules are artifacts in the Microsoft Container Registry (MCR). Every version of a module exists as a tag in the Container Registry and can be listed as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list

BCPNFR23 - Module composition

ID: BCPNFR23 - Category: Composition

Each Bicep AVM module that lives within the Azure/bicep-registry-modules (BRM) repository in the avm directory MUST have the following directories and files:

  • /tests - (for unit tests and additional E2E/integration if required - e.g. Pester etc.)
    • /e2e - (all examples must deploy successfully - these will be used to automatically generate the examples in the README.md for the module)
  • /src - (for scripts and other files - e.g., scripts used by the template)
    • exampleFile.ps1
  • /modules - (for sub-modules only if used and NOT children of the primary resource - e.g. RBAC role assignments)
    • exampleTemplate.bicep
  • /main.bicep (AVM Module main .bicep file and entry point/orchestration module)
  • /main.json (auto generated and what is published to the MCR via BRM)
  • /version.json (BRM requirement)
  • /README.md (auto generated AVM Module documentation)
  • /CHANGELOG.md (manually maintained changelog file with one entry per published version)

Directory and File Structure Example

/ Root of Azure/bicep-registry-modules
β”‚
β”œβ”€β”€β”€avm
β”‚   β”œβ”€β”€β”€ptn
β”‚   β”‚   └───apptiervmss
β”‚   β”‚       β”‚   main.bicep
β”‚   β”‚       β”‚   main.json
β”‚   β”‚       β”‚   README.md
β”‚   β”‚       β”‚   CHANGELOG.md
β”‚   β”‚       β”‚   version.json
β”‚   β”‚       β”œβ”€β”€β”€src (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€Get-Cake.ps1
β”‚   β”‚       β”‚   └───Find-Waldo.ps1
β”‚   β”‚       β”œβ”€β”€β”€modules (optional)
β”‚   β”‚       β”‚   β”œβ”€β”€β”€helper.bicep
β”‚   β”‚       β”‚   └───role-assignment.bicep
β”‚   β”‚       └───tests
β”‚   β”‚           β”œβ”€β”€β”€unit (optional)
β”‚   β”‚           └───e2e
β”‚   β”‚               β”œβ”€β”€β”€defaults
β”‚   β”‚               β”œβ”€β”€β”€waf-aligned
β”‚   β”‚               └───max
β”‚   β”‚
β”‚   └───res
β”‚       └───compute
β”‚           └───virtual-machine
β”‚               β”‚   main.bicep
β”‚               β”‚   main.json
β”‚               β”‚   README.md
β”‚               β”‚   CHANGELOG.md
β”‚               β”‚   version.json
β”‚               β”œβ”€β”€β”€src (optional)
β”‚               β”‚   β”œβ”€β”€β”€Set-Bug.ps1
β”‚               β”‚   └───Invoke-Promotion.ps1
β”‚               β”œβ”€β”€β”€modules (optional)
β”‚               β”‚   β”œβ”€β”€β”€helper.bicep
β”‚               β”‚   └───role-assignment.bicep
β”‚               └───tests
β”‚                   β”œβ”€β”€β”€unit (optional)
β”‚                   └───e2e
β”‚                       β”œβ”€β”€β”€defaults
β”‚                       β”œβ”€β”€β”€waf-aligned
β”‚                       └───max
β”œβ”€β”€β”€other repo dirs...
└───other repo files...

BCPNFR24 - Deterministic Deployment Names

ID: BCPNFR24 - Category: Naming/Composition - Deterministic Deployment Names

When a module references child, utility, or other modules, the deployment name MUST be deterministic. This means the deployment name must produce the same value for the same set of inputs across repeated deployments.

Why deterministic?

Azure Resource Manager has an 800-deployment limit per scope (resource group, subscription, management group, tenant). Non-deterministic names (e.g., those incorporating timestamps or utcNow()) create a new deployment object on every run, which can lead to this limit being reached over time.

While an automatic cleanup process exists for resource group and subscription scopes, it can take some time to take effect. Due to eventual consistency in the backend, the deployment count may not reflect the cleanup immediately, which can lead to failed deployments even when the actual number of deployments is below the 800 limit. Additionally, automatic cleanup does not apply to management group or tenant scopes.

We are actively working with the product team to enhance the cleanup process. In the meantime, deterministic deployment names provide a reliable way to keep deployment counts stable by overwriting previous deployment objects rather than creating new ones.

Deterministic deployment names cause Azure to overwrite the previous deployment object, keeping the deployment count stable regardless of how many times the module is deployed.

Requirement

Module owners MUST construct deployment names for referenced modules using uniqueString() seeded with the parent resource’s ID (<parentResource>.id) and location, rather than deployment().name, subscription().id, resourceGroup().id, utcNow(), or other non-deterministic or scope-level values.

The deployment name MUST follow the pattern:

'${uniqueString(<parentResource>.id, location)}-<ChildModuleDescriptor>-${index}'

Where:

SegmentDescription
uniqueString(<parentResource>.id, location)A deterministic hash derived from the parent resource’s resource ID and deployment location. This is both unique per resource instance and stable across deployments.
<ChildModuleDescriptor>A short, human-readable label identifying the child module being deployed (e.g., DB, Subnet, FederatedIdentityCred).
${index}The loop index variable, included when deploying in a loop. Omit for single (non-looped) deployments.
location parameter

If location is not available, for example when deploying a global resource that does not have a location property, it is acceptable to omit it. However, the <parentResource>.id MUST always be included as the primary seed for uniqueString.

Why parent resource ID?

Using the parent resource’s ID as the uniqueString seed provides two critical properties:

  1. Deterministic β€” the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. Collision-free β€” different parent resource instances produce different hashes, so deploying multiple instances of the same module type within the same scope does not cause naming collisions.
Why not subscription().id and resourceGroup().id separately?

The parent resource’s ID (e.g., /subscriptions/.../resourceGroups/.../providers/.../resourceName) already contains the subscription ID and resource group ID as segments. Using <parentResource>.id as a single input to uniqueString captures all of this context in one value, keeping the code concise and readable rather than passing multiple scope-level values separately.

Supporting multiple deployments of the same module at the same scope

A common scenario is deploying the same module type more than once within the same scope β€” for example, two different SQL servers each with their own set of databases, or two user-assigned identities each with their own federated credentials. Because the parent resource ID is unique per resource instance, the resulting deployment names will differ even when the child module type and index are identical. This ensures that parallel deployments of the same module at the same scope do not collide.

Other approaches fail on one or both of these properties:

ApproachDeterministic?Collision-free?Issue
deployment().nameβŒβœ…Changes every deployment; hits 800-limit
utcNow() / timestampsβŒβœ…Changes every deployment; hits 800-limit
subscription().id + resourceGroup().idβœ…βŒSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationβœ…βœ…Recommended β€” stable and unique per instance

Examples

Example 1: Single child module deployment

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_database 'database/main.bicep' = {
  name: '${uniqueString(server.id, location)}-Sql-DB'
  params: {
    serverName: server.name
    (...)
  }
}

Example 2: Child module deployment in a loop

resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]

BCPNFR3 - Usage Example formats

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <resourceName> 'br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<' = {
      name: '${uniqueString(deployment().name, location)}-test-<uniqueIdentifier>'
      params: { (...) }
    }
  • JSON / ARM Template Parameter Files - .json

    {
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
      "contentVersion": "1.0.0.0",
      "parameters": { (...) }
    }
Note

The above formats are currently automatically taken & generated from the tests/e2e tests. It is enough to run the Set-ModuleReadMe or Set-AVMModule functions (from the utilities folder) to update the usage examples in the readme(s).

Note

Bicep Parameter Files (.bicepparam) are being reviewed and considered by the AVM team for the usability and features at this time and will likely be added in the future.

BCPNFR4 - Parameter Input Examples

ID: BCPNFR4 - Category: Documentation - Parameter Input Examples

Bicep modules MAY provide parameter input examples for parameters using the metadata.example property via the @metadata() decorator.

Example:

@metadata({
  example: 'uksouth'
})
@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@metadata({
  example: '''
  {
    keyName: 'myKey'
    keyVaultResourceId: '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/myvault'
    keyVersion: '6d143c1a0a6a453daffec4001e357de0'
    userAssignedIdentityResourceId '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity'
  }
  '''
})
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyType

It is planned that these examples are automatically added to the module readme’s parameter descriptions when running either the Set-ModuleReadMe or Set-AVMModule scripts (available in the utilities folder).

BCPNFR5 - Role Assignments Role Definition Mapping Limits

ID: BCPNFR5 - Category: Composition - Role Assignments Role Definition Mapping Limits

As per BCPFR2, module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID.

Module owners SHOULD NOT map every RBAC Role Definition within this variable as it can cause the module to bloat in size and cause consumption issues later when stitched together with other modules due to the 4MB ARM Template size limit.

Therefore module owners SHOULD only map the most applicable and common RBAC Role Definition names for their module and SHOULD NOT exceed 15 RBAC Role Definitions in the variable.

Important

Remember if the RBAC Role Definition name is not included in the variable this does not mean it cannot be declared, used and assigned to an identity via an RBAC Role Assignment as part of a module, as any RBAC Role Definition can be specified via its ID without being in the variable.

Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.

BCPNFR6 - Role Assignments Role Definition Mapping Compulsory Roles

ID: BCPNFR6 - Category: Composition - Role Assignments Role Definition Mapping Compulsory Roles

Module owners MUST include the following roles in the variable for RBAC Role Definition names:

  • Owner - ID: 8e3af657-a8ff-443c-a75c-2fe8c4bcb635
  • Contributor - ID: b24988ac-6180-42a0-ab88-20f7382dd24c
  • Reader - ID: acdd72a7-3385-48ef-bd42-f606fba81ae7
  • User Access Administrator - ID: 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9
  • Role Based Access Control Administrator (Preview) - ID: f58310d9-a9f6-439a-9e8d-f62e7b41a168
Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.

BCPNFR7 - Parameter Requirement Types

ID: BCPNFR7 - Category: Inputs - Parameter Requirement Types

Modules will have lots of parameters that will differ in their requirement type (required, optional, etc.). To help consumers understand what each parameter’s requirement type is, module owners MUST add the requirement type to the beginning of each parameter’s description. Below are the requirement types with a definition and example for the description decorator:

Parameter Requirement TypeDefinitionExample Description Decorator
RequiredThe parameter value must be provided. The parameter does not have a default value and hence the module expects and requires an input.@description('Required. <PARAMETER DESCRIPTION HERE...>')
ConditionalThe parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. Should contain a sentence starting with ‘Required if (…).’ to explain the condition.@description('Conditional. <PARAMETER DESCRIPTION HERE...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe parameter value is generated within the module and should not be specified as input in most cases. A common example of this is the utcNow() function that is only supported as the input for a parameter value, and not inside a variable.@description('Generated. <PARAMETER DESCRIPTION HERE...>')

BCPNFR8 - Code Styling - lower camelCasing

ID: BCPNFR8 - Category: Composition - Code Styling - lower camelCasing

Module owners SHOULD use lower camelCasing for naming the following:

  • Parameters
  • Variables
  • Outputs
  • User Defined Types
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: camelCasingExample (lowercase first word (entirely), with capital of first letter of all other words and rest of word in lowercase)

BCPNFR9 - Inputs - Decorators

ID: BCPNFR9 - Inputs - Decorators

Similar to BCPNFR21, input parameters MUST implement decorators such as description & secure (if sensitive).

Further, input parameters SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.

@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
param threshold: int?
@description('Required. The SKU of your resource.')
@allowed([
'Basic'
'Premium'
'Standard'
])
param sku string

BCPRMNFR1 - Expected Test Directories

ID: BCPRMNFR1 - Category: Testing - Expected Test Directories

Module owners MUST create the defaults, waf-aligned folders within their /tests/e2e/ directory in their resource module source code and SHOULD create a max folder also. Module owners CAN create additional folders as required. Each folder will be used as described for various test cases.

Note

If a module can deploy varying styles of the same resource, e.g., VMs can be Linux or Windows, each style should be tested as both defaults and waf-aligned. Each must then be used as suffixes in the directory name to denote the style, e.g., for a VM we would expect to see:

  • /tests/e2e/linux.defaults/main.test.bicep
  • /tests/e2e/linux.waf-aligned/main.test.bicep
  • /tests/e2e/windows.defaults/main.test.bicep
  • /tests/e2e/windows.waf-aligned/main.test.bicep

Defaults tests (MUST)

The defaults folder contains a test instance that deploys the module with the minimum set of required parameters.

This includes input parameters of type Required plus input parameters of type Conditional marked as required for WAF compliance.

This instance has heavy reliance on the default values for other input parameters. Parameters of type Optional SHOULD NOT be used.

WAF aligned tests (MUST)

The waf-aligned folder contains a test instance that deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.

This includes input parameters of type Required, parameters of type Conditional marked as required for WAF compliance, and parameters of type Optional useful for WAF compliance.

Parameters and dependencies which are not needed for WAF compliance, SHOULD NOT be included.

Max tests (SHOULD)

The max folder contains a test instance that deploys the module using a large parameter set, enabling most of the modules’ features.

The purpose of this instance is primarily parameter validation and not necessarily to serve as a real example scenario. Ideally, all features, extension resources and child resources should be enabled in this test, unless not possible due to conflicts, e.g., in case parameters are mutually exclusive.

Note

Please note that this test is not mandatory to have, but recommended for bulk parameter validation. It can be skipped in case the module parameter validation is covered already by additional, more scenario-specific tests.

Additional tests (CAN)

Additional folders CAN be created by module owners as required.

For example, to validate parameters not covered by the max test due to conflicts, or to provide a real example scenario for a specific use case.

BCPRMNFR2 - User-defined types - AVM-Common-Types

ID: BCPRMNFR2 - User-defined types - AVM-Common-Types

When implementing any of the Bicep interface variants you MUST import their User-defined type (UDT) via the published AVM-Common-Types module.

When doing so, each type MUST be imported separately, right above the parameter or output that uses it.

import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. Array of role assignments to create.')
param roleAssignments roleAssignmentType[]?
import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. The diagnostic settings of the service.')
param diagnosticSettings diagnosticSettingFullType[]?

Importing them individually as opposed to one common block has several benefits such as

  • Individual versioning of types
  • If you must update the version for one type, you’re not exposed to unexpected changes to other types
Important

The import (...) block MUST not be added in between a parameter’s definition and its metadata. Doing so breaks the metadata’s binding to the parameter in question.

Finally, you should check for version updates regularly to ensure the resource module stays consistent with the specs. If the used AVM-Common-Types runs stale, the CI may eventually fail the module’s static tests.

BCPRMNFR3 - Child resources structure

ID: BCPRMNFR3 - Implementing child resources

Child resource modules MUST be stored in a subfolder of their parent resource module and named after the child resource’s singular name (ref), so that the path to the child resource folder is consistent with the hierarchy of its resource type.
For example, Microsoft.Sql/servers may have dedicated child resources of type Microsoft.Sql/servers/databases. Hence, the SQL server database child module is stored in a database subfolder of the server parent folder.

sql
└─ server [module]
  └─ database [child-module/resource]

In this folder, we recommend to place the child resource-template alongside a ReadMe & compiled JSON (to be generated via the default Set-AVMModule utility) and optionally further nest additional folders for its child resources.

There are several reasons to structure a module in this way. For example:

  • It allows a separation of concerns where each module can focus on its own properties and logic, while delegating most of a child-resource’s logic to its separate child module
  • It’s consistent with the provider namespace structure and makes modules easier to understand not only because they’re more aligned with set structure, but also are aligned with one another
  • As each module is its own ‘deployment’, it reduces limitations around nested loops
  • It enables module owners to publish child-modules as separate modules to the public registry, allowing consumers to make use of them directly [Ref child module publishing guidelines for details].
Note

In full transparency: The drawbacks of these additional deployments is an extended deployment period & a contribution to the 800 deployments limit. However, for AVM resource modules it was agreed that the advantages listed above outweigh these limitations.

BCPRMNFR4 - Multi-scope modules

ID: BCPRMNFR4 - Implementing multi-scope modules

Several resource types in Azure (e.g., role-assignments, budgets, etc.) may be deployed to more than one scope (e.g., subscription, management-group, etc.).
In AVM, such modules can be implemented in one of two ways:

  1. As pattern modules with one ‘orchestrating’ parent module using scoped sub-modules based on the input parameters provided

Note: Only the parent module is published. I.e., it is not possible to target e.g., the resource-group scoped sub-module directly.

  1. As resource modules where each scope is implemented as a child-module of a non-published parent.

Note: Each child module is published, but not the parent. I.e., it is possible to target e.g., the resource-group scoped sub-module directly.

Tip

It is highly recommended to publish multi-scoped modules as resource modules as the solution provides the best user experience.

Considerations when published as a pattern module

Example: avm/ptn/authorization/role-assignment

Note

The following instructions consider all deployment scopes. Your module may only deploy to a subset of the same and you should map the conventions to your case.

To successfully implement a multi-scoped module as a pattern modules you have to adhere to the following convention:

  • The parent module MUST be implemented in the highest scope the resource provider supports (e.g., management-group)
  • The parent module MUST have one sub-module for each scope that the resource provider supports (e.g., management-group, subscription & resource-group)
  • Each sub-module MUST be implemented for the scope it is intended
  • The parent module MUST invoke each sub-module in the scope it is written for, using input parameters needed to target set scope (e.g., a subscription-id to invoke a module for set scope)
  • The parent module MUST have test cases to validate each sub-module
  • The parent module is the one that is versioned, published and maintains a changelog

The full folder structure may look like

πŸ“„main.bicep                 [Orchestrating module]
πŸ“„main.json                  [ARM JSON file of the module]
πŸ“„version.json               [Version file of the module]
πŸ“„README.md                  [Readme of the module]
πŸ“„CHANGELOG.md               [The changelog of the module]
┣ πŸ“‚modules
┃ ┣ πŸ“„management-group.bicep [Sub-module deploying to the mgmt-group scope (if applicable)]
┃ ┣ πŸ“„subscription.bicep     [Sub-module deploying to the subscription scope (if applicable)]
┃ β”— πŸ“„resource-group.bicep   [Sub-module deploying to the resource-group scope (if applicable)]
β”— πŸ“‚tests/e2e
  ┣ πŸ“‚ mg.defaults
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template]
  ┣ πŸ“‚ mg.waf-aligned
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template]
  ┣ πŸ“‚ sub.defaults
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` param]
  ┣ πŸ“‚ sub.waf-aligned
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` param]
  ┣ πŸ“‚ rg.defaults
  ┃ β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` & `resourceGroupName` params]
  β”— πŸ“‚ rg.waf-aligned
    β”— πŸ“„main.test.bicep      [deploys parent template with `subscriptionId` & `resourceGroupName` params]
Warning

Even if a consumer wants to deploy to one of the sub-scopes (e.g., subscription), the module must be deployed via its parent (e.g., management-group). This can be confusing for consumers at first and should be considered when implementing the solution.

Example: To use a role-assignment pattern module (which would be written for all scopes, with the parent targeting the management-group scope) to deploy role assignments to a resource group, a user would need to invoke New-AzManagementGroupDeployment and provide the parameters for both the subscription & resource-group to target. I.e., the user must have permissions to deploy to each scope.

Considerations when published as a resource module

Example: avm/res/authorization/role-assignment

Note

The following instructions consider all deployment scopes. Your module may only deploy to a subset of the same and you should map the conventions to your case.

To successfully implement a multi-scoped module as a resource modules you have to adhere to the following convention:

  • The parent folder MUST contain a

    • main.bicep file
    • main.json file
    • README.md file
    • tests/e2e folder
    • One folder per each scope the resource provider can deploy to (either mg-scope, sub-scope or rg-scope).
  • Each child-module folder MUST be implemented as a proper child module, with a

    • main.bicep
    • main.json
    • version.json
    • README.md
    • CHANGELOG.md

    file. Each child-module is maintained and versioned independently of the others.

  • The parent main.bicep MUST contain the following information

    metadata name = '<Module Name> (Multi-Scope)'
    metadata description = '''
    This module's child-modules deploy a <Placeholder> at a Management Group (mg-scope), Subscription (sub-scope) or   Resource Group (rg-scope) scope.
    
    > While this template is **not** published, you can find the actual published modules in the subfolders
    > - `mg-scope`
    > - `sub-scope`
    > - `rg-scope`
    '''
    targetScope = 'managementGroup'

    updated with your module’s specifics

  • The tests/e2e folder MUST contain one instance of the require test cases per each scope, and MAY contain any additional test you see fit. In each case, the scope MUST be a prefix for the folder name. Each test case MUST reference the corresponding child module directly.

The full folder structure may look like

πŸ“„main.bicep                [Skeleton module with disclaimer referring to the child-modules]
πŸ“„main.json                 [ARM JSON file of the module]
πŸ“„README.md                 [The baseline readme, surfacing the metadata of the main.bicep file]
┣ πŸ“‚mg-scope
┃ β”£πŸ“„main.bicep             [Module deploying to mg-scope]
┃ β”£πŸ“„main.json              [ARM JSON file of the module]
┃ β”£πŸ“„README.md              [Readme of the module]
┃ β”£πŸ“„version.json           [Version file of the module]
┃ β”—πŸ“„CHANGELOG.md           [The changelog of the module]
┣ πŸ“‚sub-scope
┃ β”£πŸ“„main.bicep             [Module deploying to sub-scope]
┃ β”£πŸ“„main.json              [ARM JSON file of the module]
┃ β”£πŸ“„README.md              [Readme of the module]
┃ β”£πŸ“„version.json           [Version file of the module]
┃ β”—πŸ“„CHANGELOG.md           [The changelog of the module]
┣ πŸ“‚rg-scope
┃ β”£πŸ“„main.bicep             [Module deploying to rg-scope]
┃ β”£πŸ“„main.json              [ARM JSON file of the module]
┃ β”£πŸ“„README.md              [Readme of the module]
┃ β”£πŸ“„version.json           [Version file of the module]
┃ β”—πŸ“„CHANGELOG.md           [The changelog of the module]
β”— πŸ“‚tests/e2e
  ┣ πŸ“‚mg-scope.defaults
  ┃ β”—πŸ“„main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ πŸ“‚mg-scope.waf-aligned
  ┃ β”—πŸ“„main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ πŸ“‚mg-scope.max
  ┃ β”—πŸ“„main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ πŸ“‚sub-scope.defaults
  ┃ β”—πŸ“„main.test.bicep      [references the 'sub-scope' child module template: '../../../sub-scope/main.bicep']
  ┣ πŸ“‚sub-scope.waf-aligned
  ┃ β”—πŸ“„main.test.bicep      [references the 'sub-scope' child module template: '../../../sub-scope/main.bicep']
  ┣ πŸ“‚rg-scope.defaults
  ┃ β”—πŸ“„main.test.bicep      [references the 'rg-scope' child module template: '../../../rg-scope/main.bicep']
  β”— πŸ“‚rg-scope.waf-aligned
    β”—πŸ“„main.test.bicep      [references the 'rg-scope' child module template: '../../../rg-scope/main.bicep']
Important

Because each child-module is published on its own, you must ensure that each is registered in the MAR-file before the modules can be published. The MAR-file can only be accessed by Microsoft FTEs.

Please highlight the nature of your module in the issue when proposing it to AVM.

Module Classifications

Module Classification Definitions

AVM defines two module classifications, Resource Modules and Pattern Modules, that can be created, published, and consumed, these are defined further in the table below:

Module ClassDefinitionWho is it for?
Resource ModuleDeploys a primary resource with WAF high priority/impact best practice configurations set by default, e.g., availability zones, firewall, enforced Entra ID authentication and other shared interfaces, e.g., RBAC, Locks, Private Endpoints etc. (if supported). See What does AVM mean by “WAF Aligned”?

They MAY include related resources, e.g. VM contains disk & NIC. Focus should be on customer experience. A customer would expect that a VM module would include all required resources to provision a VM.

Furthermore, Resource Modules MUST NOT deploy external dependencies for the primary resource. E.g. a VM needs a vNet and Subnet to be deployed into, but the vNet will not be created by the VM Resource Module.

Finally, a resource can be anything such as Microsoft Defender for Cloud Pricing Plans, these are still resources in ARM and can therefore be created as a Resource Module.
People who want to craft bespoke architectures that default to WAF best practices, where appropriate, for each resource.

People who want to create pattern modules.
Pattern ModuleDeploys multiple resources, usually using Resource Modules. They can be any size but should help accelerate a common task/deployment/architecture.

Good candidates for pattern modules are those architectures that exist in Azure Architecture Center, or other official documentation.

Note: Pattern modules can contain other pattern modules, however, pattern modules MUST NOT contain references to non-AVM modules.
People who want to easily deploy patterns (architectures) using WAF best practices.
Utility Module
(draft,
see below)
Implements a function or routine that can be flexibly reused in resource or pattern modules - e.g., a function that retrieves the endpoint of an API or portal of a given environment.

It MUST NOT deploy any Azure resources other than deployment scripts.
People who want to leverage commonly used functions/routines/helpers in their module, instead of re-implementing them locally.
PREVIEW

The concept of Utility Modules will be introduced gradually, through some initial examples. The definition above is subject to change as additional details are worked out.

The required automated tests and other workflow elements will be derived from the Pattern Modules’ automation/CI environment as the concept matures.

Utility modules will follow the below naming convention:

  • Bicep: avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name>. Modules will be kept under the avm/utl folder in the BRM repository.
  • Terraform: avm-utl-<utility-module-name>. Repositories will be named after the utility module (e.g., terraform-azurerm-avm-utl-<my utility module>).

All related documentation (functional and non-functional requirements, etc.) will also be published along the way.

Module Lifecycle

This section outlines the different stages of a module’s lifecycle:

flowchart LR
    Proposed["1 - Proposed βšͺ"] --> |Acceptance criteria met βœ…| Available["2 - Available 🟒"]
      click Proposed "/Azure-Verified-Modules/specs/shared/module-lifecycle/#1-proposed-modules"
      click Available "/Azure-Verified-Modules/specs/shared/module-lifecycle/#2-available-modules"
    Proposed --> |Acceptance criteria not met ❌| Rejected[Rejected]
    Available --> |Module temporarily not maintained| Orphaned["3 - Orphaned 🟑"]
    Orphaned --> |End of life| Deprecated["4 - Deprecated πŸ”΄"]
      click Orphaned "/Azure-Verified-Modules/specs/shared/module-lifecycle/#3-orphaned-modules"
    Orphaned --> |New owner identified| Available
    Available --> |End of life| Deprecated
      click Deprecated "/Azure-Verified-Modules/specs/shared/module-lifecycle/#4-deprecated-modules"
    style Proposed fill:#ADD8E6,stroke:#333,stroke-width:1px
    style Orphaned fill:#F4A460,stroke:#333,stroke-width:1px
    style Available fill:#8DE971,stroke:#333,stroke-width:4px
    style Deprecated fill:#000000,stroke:#333,stroke-width:1px,color:#fff
    style Rejected fill:#A2A2A2,stroke:#333,stroke-width:1px
Important

If a module proposal is rejected, the issue is closed and the module’s lifecycle ends.

1. Proposed Modules

A module can be proposed through the module proposal process. The module proposal process is outlined in the Process Overview section.

To propose/request a new AVM resource, pattern or utility module, submit a module proposal issue in the AVM repository.

The proposal should include the following information:

  • module name
  • language (Bicep, Terraform, etc.)
  • module class (resource, pattern, utility)
  • module description
  • module owner(s) - if known

The AVM core team will review the proposal, and administrate the module.

Info

To propose a new module, submit a module proposal issue in the AVM repository.

2. Available modules

Once a module has been fully developed, tested and published in the main branch of the repository and the corresponding public registry (Bicep or Terraform), it is then considered to be “available” and can be used by the community. The module is maintained by the module owner(s). Feature or bug fix requests and related pull requests can be submitted by anyone to the module owner(s) for review.

Info

To publish a new version of an existing module (i.e., anything that is not being published for the first time ever), there’s no need to submit any issues in the AVM repository; contributors can just submit a Pull Request in the module’s repository with the suggested changes.

βž• Who needs to approve the PR?

The PR approval logic for existing modules is the following:

PR is submitted by a module ownerPR is submitted by anyone, other than the module owner
Module has a single module ownerAVM core team or in case of Terraform only, the owner of another module approves the PRModule owner approves the PR
Module has multiple module ownersAnother owner of the module (other than the submitter) approves the PROne of the owners of the module approves the PR

This behavior is assisted by bots, through automatic assignment of the expected reviewer(s) and supporting labels.

In case of Bicep modules, if the PR includes any changes outside of the “modules/” folder, it first needs the module related code changes need to be reviewed and approved as per the above table, and only then does the PR need to be approved by a member of the core team. This way the core team’s approval does not act as a bypass from the actual code review perspective.

3. Orphaned Modules

It is critical to the consumers experience that modules continue to be maintained. In the case where a module owner cannot continue in their role or do not respond to issues as per the defined timescale in the Module Support page , the following process will apply:

  1. The module owner is responsible for finding a replacement owner and providing a handover.
  2. If no replacement can be found or the module owner leaves Microsoft without giving warning to the AVM core team, the AVM core team will provide essential maintenance (critical bug and security fixes), as per the Module Support page
  3. The AVM core team will continue to try and re-assign the module ownership.
  4. While a module is in an orphaned state, only security and bug fixes MUST be made, no new feature development will be worked on until a new owner is found that can then lead this effort for the module.
  5. An issue will be created on the central AVM repo (Azure/Azure-Verified-Modules) to track the finding of a new owner for a module.
Info

To orphan a module, submit an orphaned module issue in the AVM repository. For the required steps, review the related article: When a module becomes orphaned.

When a new owner is identified, follow the related guidance.

Notification of a Module Becoming Orphaned

Important

When a module becomes orphaned, the AVM core team will communicate this through an information notice to be placed as follows.

  • In case of a Bicep module, the information notice will be placed in an ORPHANED.md file and in the header of the module’s README.md - both residing in the module’s root.
  • In case of a Terraform module, the information notice will be placed in the header of the README.md file, in the module’s root.

The information notice will include the following statement:

⚠️THIS MODULE IS CURRENTLY ORPHANED.⚠️

- Only security and bug fixes are being handled by the AVM core team at present.
- If interested in becoming the module owner of this orphaned module (must be Microsoft FTE), please look for the related "orphaned module" GitHub issue [here](https://aka.ms/AVM/OrphanedModules)!

Also, the AVM core team will amend the issue automation to auto reply stating that the repo is orphaned and only security/bug fixes are being handled until a new module owner is found.

4. Deprecated Modules

Once a module reaches the end of its lifecycle (e.g., it’s permanently replaced by another module; permanent retirement due to obsolete technology/solution), it needs to be deprecated. A deprecated module will no longer be maintained, and no new features or bug fixes will be implemented for it. The module will indefinitely stay available in the public registry and source code repository for use, but certain measures will take place, such as:

  1. The module will show as deprecated in the AVM module index.
  2. The module will no longer be shown through VS Code IntelliSense.
  3. The module’s source code will be kept in its repository but it will show a deprecated status through a DEPRECATED.md file (Bicep only) and a disclaimer in the module’s README.md file.
  4. It will be a clearly indicated on the module’s repo that new issues can no longer be submitted for the module:
    • Bicep: The module will be taken off the list of available modules in related issue templates.
    • Terraform: The module’s repo will be archived.

It is recommended to migrate to a replacement/alternative version of the module, if available.

Important

When a module becomes deprecated, the AVM core team will communicate this through an information notice to be placed as follows.

  • In case of a Bicep module, the information notice will be placed in a DEPRECATED.md file and in the header of the module’s README.md - both residing in the module’s root.
  • In case of a Terraform module, the information notice will be placed in the header of the README.md file, in the module’s root.

The information notice MUST include the following statement:

⚠️THIS MODULE IS DEPRECATED.⚠️

- It will no longer receive any updates.
- If the underlying Azure service is not deprecated/retired, this module may still be used as is (references to any existing versions will keep working), but it is not recommended for new deployments.
- It is recommended to migrate to a replacement/alternative version of the module, if available.
Info

To deprecate a module, submit a deprecated module issue in the AVM repository. For the required steps, review the related article: When a module becomes deprecated.

βž• Retrieve the available versions of a deprecated module

To find all previous versions of a Bicep module, the following steps need to be performed (assuming the avm/ptn/finops-toolkit/finops-hub module has been deprecated):

  1. To find out the all the versions the module has ever been published under, perform one of these steps:
    1. navigate to Bicep Public Registry’s JSON index and look for the module’s name,
    2. OR visit https://mcr.microsoft.com/v2/bicep/avm/ptn/finops-toolkit/finops-hub/tags/list.
    3. OR clone the Bicep Public Registry repository and run the following command in the root of the repository: git tag -l 'avm/ptn/finops-toolkit/finops-hub/*'. This will list all the tags that match the module’s name.
  2. Identify the available versions of the module, e.g., 0.1.0, 0.1.1, etc.
  3. To download the content, construct and navigate to the following URL: https://github.com/Azure/bicep-registry-modules/releases/tag/avm/ptn/finops-toolkit/finops-hub/0.1.0
  4. To see the content in the folder hierarchy, construct and navigate to the following URL: https://github.com/Azure/bicep-registry-modules/tree/avm/ptn/finops-toolkit/finops-hub/0.1.0/avm/ptn/finops-toolkit/finops-hu

Terraform modules will be listed in the HashiCorp Terraform Registry indefinitely.

PMNFR1 - Module Naming

ID: PMNFR1 - Category: Naming - Module Naming

Pattern Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Pattern Module Naming

  • Naming convention: avm/ptn/<hyphenated grouping/category name>/<hyphenated pattern module name>
  • Example: avm/ptn/compute/app-tier-vmss or avm/ptn/avd-lza/management-plane or avm/ptn/3-tier/web-app
  • Segments:
    • ptn defines this as a pattern module
    • <hyphenated grouping/category name> is a hierarchical grouping of pattern modules by category, with each word separated by dashes, such as:
      • project name, e.g., avd-lza,
      • primary resource provider, e.g., compute or network, or
      • architecture, e.g., 3-tier
    • <hyphenated pattern module name> is a term describing the module’s function, with each word separated by dashes, e.g., app-tier-vmss = Application Tier VMSS; management-plane = Azure Virtual Desktop Landing Zone Accelerator Management Plane

Terraform Pattern Module Naming

  • Naming convention:
    • avm-ptn-<pattern module name> (Module name for registry)
    • terraform-<provider>-avm-ptn-<pattern module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-ptn-apptiervmss or avm-ptn-avd-lza-managementplane
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • ptn defines this as a pattern module
    • <pattern module name> is a term describing the module’s function, e.g., apptiervmss = Application Tier VMSS; avd-lza-managementplane = Azure Virtual Desktop Landing Zone Accelerator Management Plane

PMNFR2 - Use Resource Modules to Build a Pattern Module

ID: PMNFR2 - Category: Composition - Use Resource Modules to Build a Pattern Module

A Pattern Module SHOULD be built from AVM Resources Modules to establish a standardized code base and improve maintainability. If a valid reason exists, a pattern module MAY contain native resources (“vanilla” code) where it’s necessary. A Pattern Module MUST NOT contain references to non-AVM modules.

Valid reasons for not using a Resource Module for a resource required by a Pattern Module include but are not limited to:

  • When using a Resource Module would result in hitting scaling limitations and/or would reduce the capabilities of the Pattern Module due to the limitations of Azure Resource Manager.
  • Developing a Pattern Module under time constraint, without having all required Resource Modules readily available.
Note

In the latter case, the Pattern Module SHOULD be updated to use the Resource Module when the required Resource Module becomes available, to avoid accumulating technical debt. Ideally, all required Resource Modules SHOULD be developed first, and then leveraged by the Pattern Module.

PMNFR4 - Missing Resource Module(s)

ID: PMNFR4 - Category: Hygiene - Missing Resource Module(s)

An item MUST be logged onto as an issue on the AVM Central Repo (Azure/Azure-Verified-Modules) if a Resource Module does not exist for resources deployed by the pattern module.

Exception

If the Resource Module adds no value, see Resource Module functional requirement ID: RMFR2.

RMFR3 - Resource Groups

ID: RMFR3 - Category: Composition - Resource Groups

A resource module MUST NOT create a Resource Group for resources that require them.

In the case that a Resource Group is required, a module MUST have an input (scope or variable):

  • In Bicep the targetScope MUST be set to resourceGroup or not specified (which means default to resourceGroup scope)
  • In Terraform the variable MUST be called resource_group_name

Scopes will be covered further in the respective language specific specifications.

RMFR4 - AVM Consistent Feature & Extension Resources Value Add

ID: RMFR4 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add

Resource modules support the following optional features/extension resources, as specified, if supported by the primary resource. The top-level variable/parameter names MUST be:

Optional Features/Extension ResourcesBicep Parameter NameTerraform Variable NameMUST/SHOULD
Diagnostic SettingsdiagnosticSettingsdiagnostic_settingsMUST
Role AssignmentsroleAssignmentsrole_assignmentsMUST
Resource LockslocklockMUST
TagstagstagsMUST
Managed Identities (System / User Assigned)managedIdentitiesmanaged_identitiesMUST
Private EndpointsprivateEndpointsprivate_endpointsMUST
Customer Managed KeyscustomerManagedKeycustomer_managed_keyMUST
Azure Monitor AlertsalertsalertsSHOULD

Resource modules MUST NOT deploy required/dependent resources for the optional features/extension resources specified above. For example, for Diagnostic Settings the resource module MUST NOT deploy the Log Analytics Workspace, this is expected to be already in existence from the perspective of the resource module deployed via another method/module etc.

Note

Please note that the implementation of Customer Managed Keys from an ARM API perspective is different across various RPs that implement Customer Managed Keys in their service. For that reason you may see differences between modules on how Customer Managed Keys are handled and implemented, but functionality will be as expected.

Module owners MAY choose to utilize cross repo dependencies for these “add-on” resources, or MAY chose to implement the code directly in their own repo/module. So long as the implementation and outputs are as per the specifications requirements, then this is acceptable.

Tip

Make sure to checkout the language specific specifications for more info on this:

RMFR5 - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas

ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas

Resource modules MUST implement a common interface, e.g. the input’s data structures and properties within them (objects/arrays/dictionaries/maps), for the optional features/extension resources:

See:

RMFR6 - Parameter/Variable Naming

ID: RMFR6 - Category: Inputs - Parameter/Variable Naming

Parameters/variables that pertain to the primary resource MUST NOT use the resource type in the name.

e.g., use sku, vs. virtualMachineSku/virtualmachine_sku

Another example for where RPs contain some of their name within a property, leave the property unchanged. E.g. Key Vault has a property called keySize, it is fine to leave as this and not remove the key part from the property/parameter name.

RMFR7 - Minimum Required Outputs

ID: RMFR7 - Category: Outputs - Minimum Required Outputs

Module owners MUST output the following outputs as a minimum in their modules:

OutputBicep Output NameTerraform Output Name
Resource Namenamename
Resource IDresourceIdresource_id
System Assigned Managed Identity Principal ID (if supported by module)systemAssignedMIPrincipalIdsystem_assigned_mi_principal_id
Tip

Module owners MAY also have to provide additional outputs depending on the IaC language, please check the language specific specs:

RMFR9 - End-of-life resource versions

ID: RMFR9 - Category: Composition - End-of-life resource versions

When a given version of an Azure resource used in a resource module reaches its end-of-life (EOL) and is no longer supported by Microsoft, the module owner SHOULD ensure that:

  1. The module is aligned with these changes and only includes supported versions of the resource. This is typically achieved through the allowed values in the parameter that specifies the resource SKU or type.
  2. The following notice is shown under the Notes section of the module’s readme.md. (If any related public announcement is available, it can also be linked to from the Notes section.):

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from the related parameters.”

  3. AND the related parameter’s description:

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from this parameter.”

RMNFR1 - Module Naming

ID: RMNFR1 - Category: Naming - Module Naming

Resource modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Note

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the correct singular names for all resource types to enable checks to utilize this list to ensure repos are named correctly. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

This will be updated quarterly, or ad-hoc as new RPs/ Resources are created and highlighted via a check failure.

Bicep Resource Module Naming

  • Naming convention (module name for registry): avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>
  • Example: avm/res/compute/virtual-machine or avm/res/managed-identity/user-assigned-identity
  • Segments:
    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute = compute, Microsoft.ManagedIdentity = managed-identity.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute/virtualMachines = virtual-machine, BUT Microsoft.Network/trafficmanagerprofiles = trafficmanagerprofile - since trafficmanagerprofiles is all lower case as per the ARM API definition.

Bicep Child Module Naming

  • Naming convention (module name for registry):avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>/ <hyphenated child resource type/<hyphenated grandchild resource type>/<etc.>

  • Example: avm/res/network/virtual-network/subnet or avm/res/storage/storage-account/blob-service/container

  • Segments:

    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network = network.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks = virtual-network.
    • <hyphenated child resource type (to be repeated for grandchildren, etc.)> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks/subnets = subnet or Microsoft.Storage/storageAccounts/blobServices/containers = blob-service/container.

Terraform Resource Module Naming

  • Naming convention:
    • avm-res-<resource provider>-<ARM resource type> (module name for registry)
    • terraform-<provider>-avm-res-<resource provider>-<ARM resource type> (GitHub repository name to meet registry naming requirements)
  • Example: avm-res-compute-virtualmachine or avm-res-managedidentity-userassignedidentity
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • res defines this is a resource module
    • <resource provider> is the resource provider’s name after the Microsoft part, e.g., Microsoft.Compute = compute.
    • <ARM resource type> is the singular version of the word after the resource provider, e.g., Microsoft.Compute/virtualMachines = virtualmachine

SFR1 - Preview Services

ID: SFR1 - Category: Composition - Preview Services

Modules MAY create/adopt public preview services and features at their discretion.

Preview API versions MAY be used when:

  • The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
    • For example, Diagnostic Settings (Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.

However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:

  • The description of each of the parameters/variables used for the preview service/feature MUST start with:
    • “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”

SFR2 - WAF Aligned

ID: SFR2 - Category: Composition - WAF Aligned

Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:

They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.

Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.

Tip

Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.

SFR3 - Deployment/Usage Telemetry

ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry

Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.

To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. (See more details on this requirement, here.)

Telemetry Information Notice

Note

The following information notice is automatically added at the bottom of the README.md file of the module when

  • Bicep: Using the utilities/tools/Set-AVMModule.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### Data Collection

The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.

Module Class Applicability

This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.

Bicep

Important

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. 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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be 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"
    }
  }
}

SFR4 - Telemetry Enablement Flexibility

ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility

The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry
Note

Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.

This general specification can be modified for some use-cases, that are language specific:

Bicep

For cross-references in resource modules, the spec BCPFR7 also applies.

Terraform

Currently, no further requirements apply.

SFR5 - Availability Zones

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support

SFR6 - Data Redundancy

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure

SNFR12 - Versions Supported

ID: SNFR12 - Category: Contribution/Support - Versions Supported

Only the latest released version of a module MUST be supported.

For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.

This avoids AVM Module owners from having to maintain multiple major release versions.

SNFR17 - Semantic Versioning

ID: SNFR17 - Category: Release - Semantic Versioning

Important

You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.

See the Bicep Contribution Guide for more information.

Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0

For example all modules should be released using a semantic version that matches this pattern: X.Y.Z

  • X == Major Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version 1.0.0.

    Note

    Releasing as version 0.1.0 initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. πŸ‘

  • Until first Major version 1.0.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).

SNFR18 - Breaking Changes

ID: SNFR18 - Category: Release - Breaking Changes

A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.

Info

Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.

There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.

Tip

See the language specific examples to find out how you can deal with deprecations in AVM modules.

SNFR19 - Registries Targeted

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

SNFR2 - E2E Testing

ID: SNFR2 - Category: Testing - E2E Testing

Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.

Each test MUST run and complete without user inputs successfully, for automation purposes.

Each test MUST also destroy/clean-up its resources and test dependencies following a run.

Tip

To see a directory and file structure for a module, see the language specific contribution guide.

Resources/Dependencies Required for E2E Tests

It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:

  • When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
  • When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.

Module owners MUST:

  • Create the required resources that their module depends upon in the test file/directory
    • They MUST either use:
      • Simple/native resource declarations/definitions in their respective IaC language,
        OR
      • Another already published AVM Module that MUST be pinned to a specific published version.
        • They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
βž• Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests

Terraform

resource "azurerm_resource_group" "example" {
  name     = "rsg-test-001"
  location = "West Europe"
}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "law-test-001"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku                 = "PerGB2018"
  retention_in_days   = 30
}

Bicep

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: 'law-test-001'
  location: resourceGroup().location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}
Skipping Deployments (SHOULD NOT)

Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.

Note

A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.

Details for use in E2E tests

You MUST add a note to the tests metadata description, which explains the excemption.

If you require that a test is skipped and add an β€œ.e2eignore” file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.

Sample filecontent:

The test is skipped, as only one instance of this service can be deployed to a subscription.
Note

For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.

The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).

SNFR20 - GitHub Teams Only

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

There MUST NOT be any GitHub repository permissions assigned to individual users.

Info

Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.

Bicep

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).

SNFR21 - Cross Language Collaboration

ID: SNFR21 - Category: Publishing - Cross Language Collaboration

When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.

SNFR22 - Parameters/Variables for Resource IDs

ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs

A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.

Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.

In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.

Special considerations for Bicep

If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.

Example for the property subnetArmId of the Cognitive Service’s property networkInjections:

If using a user-defined type, you may define a type for the networkInjections parameter like

param networkInjections networkInjectionType?

@export()
type networkInjectionType = {
  subnetResourceId: string

  // (...)
}

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: [{
      subnetArmId: networkInjections.?subnetResourceId
      // (...)
    }]
  }
}

or a resource-derived type like

param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections

resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
  // (...)
  properties: {
    // (...)
    networkInjections: networkInjections
  }
}

SNFR23 - GitHub Repo Labels

ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels

GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:

βž• AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD πŸ§‘β€πŸ’»These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention πŸ‘‹Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback πŸ‘‚Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes βš’οΈWhen an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence βš–We are looking for more evidence to make a decision on this
F64872
Needs: Triage πŸ”Maintainers need to triage still
FBCA04
Needs: Module Owner πŸ“£In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR.
FF0019
Needs: Module Contributor πŸ“£This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team πŸ§žβ€β™‚οΈThis item needs the AVM Core Team to review it
DB4503
Status: Awaiting Release To Be Cut βœ‚οΈThis is fixed in the main branch but not in the latest release, will be fixed with next release cut
800080
Status: Do Not Merge β›”Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed βœ…Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted πŸ†˜Extra attention is needed
FF4500
Status: In Triage πŸ”Picked up for triaging by an AVM core team member
D4AF37
Status: In PR πŸ‘‰This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity πŸ’€When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix πŸ’”This will not be worked on
FFFFFF
Status: Owners Identified 🀘This module has its owners identified
FBEF2A
Status: Module Available 🟒The module is published
C8E6C9
Status: Module Deprecated πŸ”΄This is a request to deprecate a module
000000
Status: Module Orphaned 🟑The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation πŸ“This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created πŸ“„This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance πŸ¦†This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug πŸ›Something isn't working
D73A4A
Type: CI πŸš€This issue is related to the AVM CI
74CFB0
Type: Documentation πŸ“„Improvements or additions to documentation
0075CA
Type: Duplicate 🀲This issue or pull request already exists
CFD3D7
Type: Feature Request βž•New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal πŸ’‘A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback πŸ™‹β€β™€οΈFurther information is requested or just some feedback
CB6BA2
Type: Security Bug πŸ”’This is a security bug
FFFF00
Type: AVM πŸ…°οΈ ✌️ β“œοΈThis is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep πŸ’ͺThis is related to the Bicep IaC language
1D73B3
Class: Resource Module πŸ“¦This is a resource module
D3D3D3
Class: Pattern Module πŸ“¦This is a pattern module
A9A9A9
Class: Utility Module πŸ“¦This is a utility module
CAD1DE
Class: Child Module πŸ“¦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

βž• Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  

SNFR24 - Testing Child, Extension & Interface Resources

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.

SNFR25 - Resource Naming

ID: SNFR25 - Category: Composition - Resource Naming

Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.

For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.

Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.

Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.

The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.

Tip

If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.

SNFR26 - Output - Parameters - Decorators

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id

@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key
# Resource output
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}

SNFR3 - AVM Compliance Tests

ID: SNFR3 - Category: Testing - AVM Compliance Tests

Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.

Important

Please note these are still under development at this time and will be published and available soon for module owners.

Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.

SNFR4 - Unit Tests

ID: SNFR4 - Category: Testing - Unit Tests

Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.

Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.

SNFR6 - Static Analysis/Linting Tests

ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests

Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.

There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.

SNFR7 - Idempotency Tests

ID: SNFR7 - Category: Testing - Idempotency Tests

Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.

Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.

For example, Virtual Machine Image names must be unique on each resource creation/update.

SNFR8 - Module Owner(s) GitHub

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.

Note

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.

SNFR9 - AVM & PG Teams GitHub Repo Permissions

ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions

A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:

Bicep

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

Important

Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.

For detailed steps, please follow this guidance.

Team Definitions & RACI

Teams

In AVM there will be multiple different teams involved throughout the initiatives lifecycle and ongoing long-term support. These teams will be listed below alongside their definitions.

Important

Individuals can be members of multiple teams, at once, that are defined below.

AVM Core Team

GitHub Team: @Azure/avm-core-team

The AVM core team are responsible for:

  1. Managing the AVM Solution: Leading and managing AVM from a technical standpoint, ensuring the maintenance and growth of the Public Bicep Registry’s repository and the Terraform Registry. Governing the lifecycle and support SLAs for all AVM modules, as well as providing overall governance and overseeing/facilitating the contribution process.
  2. Testing and quality enforcement: Developing, operating and enforcing the test framework and related tooling with all its quality gates. Providing initial reviews for all modules, making sure all standardsΒ are met.
  3. Documentation: Defining and refining principles, contribution and consumption guidelines, specifications and procedures related to AVM modules, maintaining and publishing all related documentation on the program’s public website.
  4. Community Engagement: Organizing internal and external events, such as hackathons, office hours, community calls and training events for current and future module owners and contributors. Presenting in live events both publicly and internally; publishing blog posts and videos on YouTube, etc.
  5. Security Enhancements: Facilitating the implementation and/or implementing security enhancements across all AVM repositories - through the WAF (Well-Architected Framework) framework.
  6. Supporting Module Owners: Providing day-to-day support for module owners, helping troubleshoot and manage security fixes for orphaned modules.
  7. Improving Processes and Gathering Insights: Improving automation for issue triage and management processes and lead the development of internal dashboards to gain insights into contribution and consumption metrics.
  8. Undefined tasks: Anything else not defined below for another team or in the RACI πŸ‘

The team includes both technical and non-technical team members who are all Microsoft FTEs.

Module Owners

Important

Today, module owners MUST be Microsoft FTEs. This is to ensure that within AVM the long-term support for each module can be upheld and honoured.

Module owners are responsible for:

  1. Initial module development
  2. Module Maintenance (proactive & reactive)
    • Regular updates to ensure compatibility with the latest Azure services (including supporting new API versions and referencing the newest AVM modules when applicable).
    • WAF Reliability & Security alignment
    • Bug fixes, security patches and feature improvements.
    • Ensuring long term compliance with AVM specifications
    • Implementing and improving automated testing and validation tools for new modules.
    • Improving documentation through rich examples.
  3. Ongoing module support
    • Module Issue/Pull Request Triage & Resolution
    • Module Feature Request Triage & Additions
  4. Managing additional module contributors

Ideally there SHOULD be at least 2 module owners per module who MUST be added to a GitHub Team in the Azure organization.

Module Contributors

Important

Module Contributors can be anyone in any organization. However, they must be an active contributor and supporting the Module Owners.

Module Contributors are responsible for:

  • Assisting the Module Owners with their responsibilities

Module Contributors MUST be added to a separate GitHub Team in the Azure organization that the Module Owners manage and are maintainers of.

Product Groups

GitHub Teams:

The Azure Bicep & Terraform Product Groups are responsible for:

  • Backup/Additional support for orphaned modules to the AVM Core Team
  • Providing inputs and feedback on AVM
  • Taking on feedback and feature requests on their products, Bicep & Terraform, from AVM usage
Note

We are investigating working with all Azure Product Groups as a future investment area that they take on ownership, or contribute to, the AVM modules for their service/product.

RACI

RACI Definition

R = Responsible – Those who do the work to complete the task/responsibility.

A = Accountable – The one answerable for the correct and thorough completion of the task. There must be only one accountable person per task/responsibility. Typically has ‘sign-off’.

C = Consulted – Those whose opinions are sought.

I = Informed – Those who are kept up to date on progress.

The below table defines a RACI to be adopted by all parties referenced in the table to ensure customers can trust these modules and can consume and contribute to the initiative at scale.

Action/Task/ResponsibilityModule OwnersModule ContributorsAVM Core TeamProduct GroupsNotes
Build/Construct an AVM ModuleR, AR, CC, II
Publish a Bicep AVM Module to the Bicep Public RegistryR, AC, IC, II
Publish a Terraform AVM Module to the Terraform RegistryAC, IRIPublishing is managed by the AVM core team via HCP Terraform
Manage and maintain tooling/testing frameworks pertaining to module qualityC, IC, IR, AC, I
Manage/run the AVM central backlog (module proposals, orphaned modules, test enhancements, etc.)C, IC, IR, AC, I
Provide day-to-day (BAU) module supportR, AR, CII
Provide security fixes for orphaned modulesN/AN/AR, AR, C, I

TFFR1 - Cross-Referencing Modules

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azurerm"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.

TFFR2 - Additional Terraform Outputs

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azurerm_resource_myresource.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azurerm_resource_mychildresource : key => value.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azurerm_resource_myresource.bar
  sensitive = true
}

TFFR3 - Providers - Permitted Versions

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
azapi>= 2.0< 3.0
azurerm>= 4.0< 5.0
Note

Authors MAY select either Azurerm, Azapi, or both providers in their module.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.0"
    }
  }
}

TFNFR1 - Descriptions

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  

TFNFR11 - Null Comparison Toggle

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azurerm_network_security_group" "this" {
  count               = var.security_group_id == null ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azurerm_network_security_group.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azurerm_network_security_group" "foo" {
  name                = "example-nsg"
  resource_group_name = "example-rg"
  location            = "eastus"
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azurerm_network_security_group.foo.id
  }
}

This technique SHOULD be used under this use case only.

TFNFR12 - Dynamic for Optional Nested Objects

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example from the community:

resource "azurerm_kubernetes_cluster" "main" {
  ...
  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type                      = var.identity_type
      user_assigned_identity_id = var.user_assigned_identity_id
    }
  }
  ...
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []

TFNFR13 - Default Values with coalesce/try

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)

TFNFR14 - Not allowed variables

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.

TFNFR16 - Variable Naming Rules

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.

TFNFR17 - Variables with Descriptions

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<-EOT
  - `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
  - `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
EOT
}

TFNFR18 - Variables with Types

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible, any MAY only be defined with adequate reasons.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)

TFNFR19 - Sensitive Data Variables

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.

TFNFR2 - Module Documentation Generation

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  

TFNFR20 - Non-Nullable Defaults for collection values

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.

TFNFR24 - Handling Deprecated Variables

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.

TFNFR25 - Verified Modules Requirements

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.11"
    }
  }
}

TFNFR26 - Providers in required_providers

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11.1, < 4.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11, < 4.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.11"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.11"
    }
  }
}

TFNFR27 - Provider Declarations in Modules

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
      configuration_aliases = [ azurerm.alternate ]
    }
  }
}

In the root module where we call this verified module:

provider "azurerm" {
  features {}
}

provider "azurerm" {
  alias = "alternate"
  features {}
}

module "foo" {
  source = "xxx"
  providers = {
    azurerm = azurerm
    azurerm.alternate = azurerm.alternate
  }
}

Bad example:

In verified module:

provider "azurerm" {
  # Configuration options
  features {}
}

TFNFR3 - GitHub Repo Branch Protection

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.

TFNFR30 - Handling Deprecated Outputs

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.

TFNFR31 - locals.tf for Locals Only

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.

TFNFR32 - Alphabetical Local Arrangement

ID: TFNFR32 - Category: Code Style - Alphabetical Local Arrangement

Expressions in locals block MUST be arranged alphabetically.

Good examples:

locals {
  name = coalesce(var.name, "name")
  tags = merge(var.tags, {
    env = "prod"
  })
}
locals {
  tags = merge(var.tags, {
    env = "prod"
  })
}

locals {
  name = coalesce(var.name, "name")
}

TFNFR34 - Using Feature Toggles

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azurerm_route_table" "this" {
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azurerm_route_table" "this" {
  count               = var.create_route_table ? 1 : 0
  location            = local.location
  name                = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  resource_group_name = var.resource_group_name
}

TFNFR35 - Reviewing Potential Breaking Changes

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.

TFNFR36 - Setting prevent_deletion_if_contains_resources

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.

TFNFR37 - Tool Usage by Module Owner

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.

TFNFR4 - Lower snake_casing

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)

TFNFR5 - Test Tooling

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.

TFNFR7 - Count & for_each Use

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azurerm_network_security_group" "this" {
  count               = local.create_new_security_group ? 1 : 0
  name                = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  resource_group_name = var.resource_group_name
  location            = local.location
  tags                = var.new_network_security_group_tags
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azurerm_subnet.subnet0.name }`
  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"]
}

Bad example:

resource "azurerm_subnet" "pair" {
  for_each             = var.subnet_name_set // `set(string)`, when user use `toset([azurerm_subnet.subnet0.name])`, it would cause an error.
  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"]
}

TFNFR8 - Resource & Data Block Orders

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azurerm_resource_group.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.

TFNFR9 - Module Block Order

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.

UMNFR1 - Module Naming

ID: UMNFR1 - Category: Naming - Module Naming

Utility Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Utility Module Naming

  • Naming convention: avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name>
  • Example: avm/utl/general/get-environment or avm/utl/types/avm-common-types
  • Segments:
    • utl defines this as a utility module
    • <hyphenated grouping/category name> is a hierarchical grouping of utility modules by category, with each word separated by dashes, such as: general or types
    • <hyphenated utility module name> is a term describing the module’s function, with each word separated by dashes, e.g., get-environment = to get environmental details; avm-common-types = to use common types.

Terraform Utility Module Naming

  • Naming convention:
    • avm-utl-<utility module name> (Module name for registry)
    • terraform-<provider>-avm-utl-<utility module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-utl-sku-finder or avm-utl-naming
  • Segments:
    • <provider> is the logical abstraction of various APIs used by Terraform. In most cases, this is going to be azurerm or azuread for resource modules.
    • utl defines this as a utility module
    • <utility module name> is a term describing the module’s function, e.g., sku-finder = to find available SKUs; naming = to handle naming conventions.

Subsections of Contributing

Bicep Contribution Guide

Important

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

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

Summary

This section lists AVM’s Bicep-specific contribution guidance.

Subsections of Bicep Modules

Bicep Composition

Important

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 Bicep specifications. ALL AVM modules (Resource and Pattern modules) MUST meet the respective requirements described in these specifications!

Important

Before jumping on implementing your contribution, please review the AVM Module specifications, in particular the Bicep specification page, to make sure your contribution complies with the AVM module’s design and principles.

Directory and File Structure

Each Bicep AVM module of the Azure/bicep-registry-modules (BRM) repository must be implemented as per the structure described in BCPNFR23.

For new modules, the files can be created automatically, once the parent folder exists. This example shows how to create a res module res/compute/virtual-machine.

Set-Location -Path ".\avm\"
New-Item -ItemType Directory -Path ".\res\compute\virtual-machine"
Set-AVMModule -ModuleFolderPath .\res\compute\virtual-machine

Code Styling

This section points to conventions to be followed when developing a Bicep template.

Casing

Use camelCasing as per BCPNFR8.

Input Parameters and Variables

Make sure to review all specifications of Category: Inputs/Outputs within the Bicep specification pages.

Tip

See examples in specifications SNFR14 and BCPNFR1.

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.

Tip

See examples in specifications SFR1 and RMFR1.

Modules

Modules enable you to reuse code from a Bicep file in other Bicep files. As such, for resource modules they’re normally leveraged for deploying child resources (e.g., file services in a storage account), cross referenced resources (e.g., network interface in a virtual machine) or extension resources (e.g., role assignments in a key vault). Pattern modules, normally reuse resource modules combined together.

Make sure to review all specifications covering module properties and usage.

Tip

See examples in specifications BCPFR1 for resource modules and PMNFR2 for pattern modules.

Outputs

Make sure to review all specifications of Category: Inputs/Outputs within the Bicep specific pages.

Tip

See examples in specification RMFR7.

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 Bicep Interfaces page.
If the primary resource of the AVM resource module you are developing supports any of the listed features/extension resources, please follow the corresponding provided Bicep schema to develop them.

Deprecation

Breaking changes are sometimes not avoidable. The impact should be kept as low as possible. A recommendation is to deprecate parameters, instead of completely removing them for a couple of versions. The Semantic Versioning sections offers information about versioning AVM modules.

In case you need to deprecate an input parameter, this sample shows you how this can be achieved.

Note

Since all modules are versioned, nothing will change for existing deployments, as the parameter usage does not change for any existing versions.

Example-Scenario

An AVM module is modified, and the parameters will change, which breaks backward compatibility.

  • parameters are changing to a custom type
  • the parameter structure is changing
  • backward compatibility will be maintained

Existing input parameters used to be defined as follows (reducing the examples to the minimum):

// main.bicep:
param item object?

// main.test.bicep:
name: 'name'
item:
  {
    variant: 'Large'
    osType: 'Windows'
  }

Testing

Before you begin to modify anything, it is recommended to create a new test case (e.g. deprecated), in addition to the already existing tests, to make sure that the changes are not breaking backward compatibility until you decide to finally remove the deprecated parameters (see BCPRMNFR1 - Category: Testing - Expected Test Directories for more details about the requirements).

module testDeployment '../../../main.bicep' = [
  for iteration in ['init', 'idem']: {
    scope: resourceGroup
    name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}'
    params: {
      name: '${namePrefix}${serviceShort}001'
      item: {
        variant: 'Large'
        osType: 'Linux'
      }
    }
  }
]

The test should include all previously used parameters to make sure they are covered before any changes to the new parameter layout are done.

Code Changes

The new parameter structure requires a change to the used parameters and moves them to a different location and looks like:

// main.bicep:
param item itemType?

type itemtype: {
  name: string // the name parameter did not change

  properties ={
    osType: 'Linux' | 'Windows'? // the new place for the osType

    variant: {
      size: string? // the new place for the variant size
    }?
  }

  // keep these for backward compatibility in the new type
  @description('Optional. Note: This is a deprecated property, please use the corresponding `properties.osType` instead.')
  osType: string? // the old parameter location

  @description('Optional. Note: This is a deprecated property, please use the corresponding `properties.variant.size` instead.')
  variant: string? // the old parameter location
}

The original parameter item is of type object and does not give the user any clue of what the syntax is and what is expected to be added to it. The tests could bring light into the darkness, but this is not ideal. In order to retain backward compatibility, the previously used parameters need to be added to the new type, as they would be invalid otherwise. Now that the new type is in place, some logic needs to be implemented to make sure the module can handle the different sources of data (new and old parameters).

resource <modulename> 'Microsoft.xy/yz@2024-01-01' = {
  name: name
  properties: {
    osType: item.?properties.?osType ?? item.?osType ?? 'Linux' // add a default here, if needed
    variant: {
      size: item.?properties.?variant.?size ?? item.?variant
    }
  }
}

By choosing this order for the Coalesce operator, the new format takes precedence over the old syntax. Also note the safe-dereference ensures that no null reference exception will occure if the property has optional parameters.

The tests can now be changed to adapt the new parameter structure for the new version of the module. They will not cover the old parameter structure anymore.

module testDeployment '../../../main.bicep' = [
  for iteration in ['init', 'idem']: {
    scope: resourceGroup
    name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}'
    params: {
      name: '${namePrefix}${serviceShort}001'
      location: resourceLocation
      item:{
        osType: 'Linux'
        variant: {
          size: 'Large'
        }
      }
    }
  }
]

Summary

Changes to modules (resource or pattern) can bei implemented in two ways.

  1. Implement changes with backward compatibility

    In this scenario, you need to make sure that the code does not break backward compatibility by:

    • adding new parameters
    • marking other parameters as deprecated
    • create a test case for the old usage syntax
    • increase the minor version number of the module (0.x)
  2. Introduce breaking changes

    The easier way to introduce a new major version requires fewer steps:

    • adding new parameters
    • create a test case for the usage
    • increase the major version number of the module (x.0.0)
Note

Be aware that currently no module has been released as 1.0.0 (or beyond), which lets you implement breaking changes without increasing the major version.

Bicep Contribution Flow

High-level contribution flow


---
config:
  nodeSpacing: 20
  rankSpacing: 20
  diagramPadding: 50
  padding: 5
  flowchart:
    wrappingWidth: 300
    padding: 5
  layout: elk
  elk:
    mergeEdges: true
    nodePlacementStrategy: LINEAR_SEGMENTS
---

flowchart TD
  A("1 - Fork the module source repository")
    click A "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#1-fork-the-module-source-repository"
  B(2 - Configure a deployment identity in Azure)
    click B "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#2-configure-a-deployment-identity-in-azure"
  C("3 - Configure CI environment for module tests")
    click C "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#3-configure-your-ci-environment"
  D("4 - Implementing your contribution<br>(Refer to Gitflow Diagram below)")
    click D "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#4-implement-your-contribution"
  E(5 - Workflow test completed successfully?)
    click E "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#5-createupdate-and-run-tests"
  F(6 - Create a pull request to the upstream repository)
    click F "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#6-create-a-pull-request-to-the-public-bicep-registry"
  G(7 - Get your pull request approved)
    click G "/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#7-get-your-pull-request-approved"
  A --> B
  B --> C
  C --> D
  D --> E
  E -->|yes|F
  E -->|no|D
  F --> G

GitFlow for contributors

The GitFlow process outlined here introduces a central anchor branch. This branch should be treated as if it were a protected branch. It serves to synchronize the forked repository with the original upstream repository. The use of the anchor branch is designed to give contributors the flexibility to work on several modules simultaneous.

---

config:
  logLevel: debug
  gitGraph:
    rotateCommitLabel: false
---

gitGraph LR:
  commit id:"Fork Repo"
  branch anchor
  checkout anchor
  commit id:"Sync Upstream/main" type: HIGHLIGHT
  branch avm-type-provider-resource-workflow
  checkout avm-type-provider-resource-workflow
  commit id:"Add Workflow File for Resource/Pattern"
  branch avm-type-provider-resource
  checkout main
  merge avm-type-provider-resource-workflow id: "merge workflow for GitHub Actions Testing" type: HIGHLIGHT
  checkout avm-type-provider-resource
  commit id:"Init"
  commit id:"Patch 1"
  commit id:"Patch 2"
  checkout main
  merge avm-type-provider-resource
Tip

When implementing the GitFlow process as described, it is advisable to configure the local clone with a remote for the upstream repository. This will enable the Git CLI and local IDE to merge changes directly from the upstream repository. Using GitHub Desktop, this is configured automatically when cloning the forked repository via the application.

PowerShell Helper Script To Setup Fork & CI Test Environment

Now defaults to OIDC setup

The PowerShell Helper Script has recently added support for the OIDC setup and configuration as documented in detail on this page. This is now the default for the script.

The easiest way to get yourself set back up, is to delete your fork repository, including the local clone of it that you have and start over with the script. This will ensure you have the correct setup for the OIDC authentication method for the AVM CI.

Important

To simplify the setup of the fork, clone and configuration of the required GitHub Environments, Secrets, User-Assigned Managed Identity (UAMI), Federated Credentials and RBAC assignments in your Azure environment for the CI framework to function correctly in your fork, we have created a PowerShell script that you can use to do steps 1, 2 & 3 below.

The script performs the following steps:

  1. Forks the Azure/bicep-registry-modules to your GitHub Account.
  2. Clones the repo locally to your machine, based on the location you specify in the parameter: -GitHubRepositoryPathForCloneOfForkedRepository.
  3. Prompts you and takes you directly to the place where you can enable GitHub Actions Workflows on your forked repo.
  4. Disables all AVM module workflows, as per Enable or Disable Workflows.
  5. Creates an User-Assigned Managed Identity (UAMI) and federated credentials for OIDC with your forked GitHub repo and grants it the RBAC roles of Owner at Management Group level, if specified in the -GitHubSecret_ARM_MGMTGROUP_ID parameter, and at Azure Subscription level if you provide it via the -GitHubSecret_ARM_SUBSCRIPTION_ID parameter.
  6. Creates the required GitHub Environments & required Secrets in your forked repo as per step 3, based on the input provided in parameters and the values from resources the script creates and configures for OIDC. Also set the workflow permissions to Read and write permissions as per step 3.3.

Pre-requisites

  1. You must have the Azure PowerShell Modules installed and you need to be logged with the context set to the desired Tenant. You must have permissions to create an SPN and grant RBAC over the specified Subscription and Management Group, if provided.
  2. You must have the GitHub CLI installed and need to be authenticated with the GitHub user account you wish to use to fork, clone and work with on AVM.
βž• New-AVMBicepBRMForkSetup.ps1 - PowerShell Helper Script

The New-AVMBicepBRMForkSetup.ps1 can be downloaded from here.

Once downloaded, you can run the script by running the below - Please change all the parameter values in the below script usage example to your own values (see the parameter documentation in the script itself)!:

.\<PATH-TO-SCRIPT-DOWNLOAD-LOCATION>\New-AVMBicepBRMForkSetup.ps1 -GitHubRepositoryPathForCloneOfForkedRepository "<pathToCreateForkedRepoIn>" -GitHubSecret_ARM_MGMTGROUP_ID "<managementGroupId>" -GitHubSecret_ARM_SUBSCRIPTION_ID "<subscriptionId>" -GitHubSecret_ARM_TENANT_ID "<tenantId>" -GitHubSecret_TOKEN_NAMEPREFIX "<unique3to5AlphanumericStringForAVMDeploymentNames>" -UAMIRsgLocation "<Azure Region/Location of your choice such as 'uksouth'>"

For more examples, see the below script’s parameters section.

ο»Ώ[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]

#Requires -PSEdition Core
#Requires -Modules @{ ModuleName="Az.Accounts"; ModuleVersion="2.19.0" }
#Requires -Modules @{ ModuleName="Az.Resources"; ModuleVersion="6.16.2" }

<#
.SYNOPSIS
This function creates and sets up everything a contributor to the AVM Bicep project should need to get started with their contribution to a AVM Bicep Module.

.DESCRIPTION
This function creates and sets up everything a contributor to the AVM Bicep project should need to get started with their contribution to a AVM Bicep Module. This includes:

- Forking and cloning the `Azure/bicep-registry-modules` repository
- Creating a new SPN and granting it the necessary permissions for the CI tests and configuring the forked repositories secrets, as per: https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#2-configure-a-deployment-identity-in-azure
- Enabling GitHub Actions on the forked repository
- Disabling all the module workflows by default, as per: https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/enable-or-disable-workflows/

Effectively simplifying this process to a single command, https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/

.PARAMETER GitHubRepositoryPathForCloneOfForkedRepository
Mandatory. The path to the GitHub repository to fork and clone. Directory will be created if does not already exist. Can use either relative paths or full literal paths.

.PARAMETER GitHubSecret_ARM_MGMTGROUP_ID
Optional. The group ID of the management group to test-deploy modules in. Is needed for resources that are deployed to the management group scope. If not provided CI tests on Management Group scoped modules will not work and you will need to manually configure the RBAC role assignments for the SPN and associated repository secret later.

.PARAMETER GitHubSecret_ARM_SUBSCRIPTION_ID
Mandatory. The ID of the subscription to test-deploy modules in. Is needed for resources that are deployed to the subscription scope.

.PARAMETER GitHubSecret_ARM_TENANT_ID
Mandatory. The tenant ID of the Azure Active Directory tenant to test-deploy modules in. Is needed for resources that are deployed to the tenant scope.

.PARAMETER GitHubSecret_TOKEN_NAMEPREFIX
Mandatory. Required. A short (3-5 character length), unique string that should be included in any deployment to Azure. Usually, AVM Bicep test cases require this value to ensure no two contributors deploy resources with the same name - which is especially important for resources that require a globally unique name (e.g., Key Vault). These characters will be used as part of each resource’s name during deployment.

.PARAMETER SPNName
Optional. The name of the SPN (Service Principal) to create. If not provided, a default name of `spn-avm-bicep-brm-fork-ci-<GitHub Organization>` will be used.

.PARAMETER UAMIName
Optional. The name of the UAMI (User Assigned Managed Identity) to create. If not provided, a default name of `id-avm-bicep-brm-fork-ci-<GitHub Organization>` will be used.

.PARAMETER UAMIRsgName
Optional. The name of the Resource Group to create for the UAMI (User Assigned Managed Identity) to create. If not provided, a default name of `rsg-avm-bicep-brm-fork-ci-<GitHub Organization>-oidc` will be used.

.PARAMETER UAMIRsgLocation
Optional. The location of the Resource Group to create for the UAMI (User Assigned Managed Identity) to create. Also UAMI will be created in this location. This is required for OIDC deployments.

.PARAMETER UseOIDC
Optional. Default is `$true`. If set to `$true`, the script will use the OIDC (OpenID Connect) authentication method for the SPN instead of secrets as per https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#31-set-up-secrets. If set to `$false`, the script will use the Client Secret authentication method for the SPN and not OIDC.

.EXAMPLE
.\<PATH-TO-SCRIPT-DOWNLOAD-LOCATION>\New-AVMBicepBRMForkSetup.ps1 -GitHubRepositoryPathForCloneOfForkedRepository "D:\GitRepos\" -GitHubSecret_ARM_MGMTGROUP_ID "alz" -GitHubSecret_ARM_SUBSCRIPTION_ID "1b60f82b-d28e-4640-8cfa-e02d2ddb421a" -GitHubSecret_ARM_TENANT_ID "c3df6353-a410-40a1-b962-e91e45e14e4b" -GitHubSecret_TOKEN_NAMEPREFIX "ex123" -UAMIRsgLocation "uksouth"

Example Subscription & Management Group scoped deployments enabled via OIDC with default generated UAMI Resource Group name of `rsg-avm-bicep-brm-fork-ci-<GitHub Organization>-oidc` and UAMI name of `id-avm-bicep-brm-fork-ci-<GitHub Organization>`.

.EXAMPLE
.\<PATH-TO-SCRIPT-DOWNLOAD-LOCATION>\New-AVMBicepBRMForkSetup.ps1 -GitHubRepositoryPathForCloneOfForkedRepository "D:\GitRepos\" -GitHubSecret_ARM_MGMTGROUP_ID "alz" -GitHubSecret_ARM_SUBSCRIPTION_ID "1b60f82b-d28e-4640-8cfa-e02d2ddb421a" -GitHubSecret_ARM_TENANT_ID "c3df6353-a410-40a1-b962-e91e45e14e4b" -GitHubSecret_TOKEN_NAMEPREFIX "ex123" -UAMIRsgLocation "uksouth" -UAMIName "my-uami-name" -UAMIRsgName "my-uami-rsg-name"

Example with provided UAMI Name & UAMI Resource Group Name.

.EXAMPLE
.\<PATH-TO-SCRIPT-DOWNLOAD-LOCATION>\New-AVMBicepBRMForkSetup.ps1 -GitHubRepositoryPathForCloneOfForkedRepository "D:\GitRepos\" -GitHubSecret_ARM_SUBSCRIPTION_ID "1b60f82b-d28e-4640-8cfa-e02d2ddb421a" -GitHubSecret_ARM_TENANT_ID "c3df6353-a410-40a1-b962-e91e45e14e4b" -GitHubSecret_TOKEN_NAMEPREFIX "ex123" -UseOIDC $false

DEPRECATED - USE OIDC INSTEAD.
Example Subscription scoped deployments enabled only with default generated SPN name of `spn-avm-bicep-brm-fork-ci-<GitHub Organization>`.

.EXAMPLE
.\<PATH-TO-SCRIPT-DOWNLOAD-LOCATION>\New-AVMBicepBRMForkSetup.ps1 -GitHubRepositoryPathForCloneOfForkedRepository "D:\GitRepos\" -GitHubSecret_ARM_MGMTGROUP_ID "alz" -GitHubSecret_ARM_SUBSCRIPTION_ID "1b60f82b-d28e-4640-8cfa-e02d2ddb421a" -GitHubSecret_ARM_TENANT_ID "c3df6353-a410-40a1-b962-e91e45e14e4b" -GitHubSecret_TOKEN_NAMEPREFIX "ex123" -SPNName "my-spn-name" -UseOIDC $false

DEPRECATED - USE OIDC INSTEAD.
Example with provided SPN name.

#>

[CmdletBinding(SupportsShouldProcess = $false)]
param (
  [Parameter(Mandatory = $true)]
  [string] $GitHubRepositoryPathForCloneOfForkedRepository,

  [Parameter(Mandatory = $false)]
  [string] $GitHubSecret_ARM_MGMTGROUP_ID,

  [Parameter(Mandatory = $true)]
  [string] $GitHubSecret_ARM_SUBSCRIPTION_ID,

  [Parameter(Mandatory = $true)]
  [string] $GitHubSecret_ARM_TENANT_ID,

  [Parameter(Mandatory = $true)]
  [string] $GitHubSecret_TOKEN_NAMEPREFIX,

  [Parameter(Mandatory = $false)]
  [string] $SPNName,

  [Parameter(Mandatory = $false)]
  [string] $UAMIName,

  [Parameter(Mandatory = $false)]
  [string] $UAMIRsgName = "rsg-avm-bicep-brm-fork-ci-oidc",

  [Parameter(Mandatory = $false)]
  [string] $UAMIRsgLocation,

  [Parameter(Mandatory = $false)]
  [bool] $UseOIDC = $true
)

# Check if the GitHub CLI is installed
$GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
if ($null -eq $GitHubCliInstalled) {
  throw 'The GitHub CLI is not installed. Please install the GitHub CLI and try again. Install link for GitHub CLI: https://github.com/cli/cli#installation'
}
Write-Host 'The GitHub CLI is installed...' -ForegroundColor Green

# Check if GitHub CLI is authenticated
$GitHubCliAuthenticated = gh auth status
if ($LASTEXITCODE -ne 0) {
  Write-Host $GitHubCliAuthenticated -ForegroundColor Red
  throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI command of 'gh auth login', and try again."
}
Write-Host 'Authenticated to GitHub with following details...' -ForegroundColor Cyan
Write-Host ''
gh auth status
Write-Host ''

# Ask the user to confirm if it's the correct GitHub account
do {
  Write-Host "Is the above GitHub account correct to coninue with the fork setup of the 'Azure/bicep-registry-modules' repository? Please enter 'y' or 'n'." -ForegroundColor Yellow
  $userInput = Read-Host
  $userInput = $userInput.ToLower()

  switch ($userInput) {
    'y' {
      Write-Host ''
      Write-Host 'User Confirmed. Proceeding with the GitHub account listed above...' -ForegroundColor Green
      Write-Host ''
      break
    }
    'n' {
      Write-Host ''
      throw "User stated incorrect GitHub account. Please switch to the correct GitHub account. You can do this in the GitHub CLI (gh) by logging out by running 'gh auth logout' and then logging back in with 'gh auth login'"
    }
    default {
      Write-Host ''
      Write-Host "Invalid input. Please enter 'y' or 'n'." -ForegroundColor Red
      Write-Host ''
    }
  }
} while ($userInput -ne 'y' -and $userInput -ne 'n')

# Fork and clone repository locally
Write-Host "Changing to directory $GitHubRepositoryPathForCloneOfForkedRepository ..." -ForegroundColor Magenta

if (-not (Test-Path -Path $GitHubRepositoryPathForCloneOfForkedRepository)) {
  Write-Host "Directory does not exist. Creating directory $GitHubRepositoryPathForCloneOfForkedRepository ..." -ForegroundColor Yellow
  New-Item -Path $GitHubRepositoryPathForCloneOfForkedRepository -ItemType Directory -ErrorAction Stop
  Write-Host ''
}
Set-Location -Path $GitHubRepositoryPathForCloneOfForkedRepository -ErrorAction stop
$CreatedDirectoryLocation = Get-Location
Write-Host "Forking and cloning 'Azure/bicep-registry-modules' repository..." -ForegroundColor Magenta

gh repo fork 'Azure/bicep-registry-modules' --default-branch-only --clone=true
if ($LASTEXITCODE -ne 0) {
  throw "Failed to fork and clone the 'Azure/bicep-registry-modules' repository. Please check the error message above, resolve any issues, and try again."
}

$ClonedRepoDirectoryLocation = Join-Path $CreatedDirectoryLocation 'bicep-registry-modules'
Write-Host ''
Write-Host "Fork of 'Azure/bicep-registry-modules' created successfully directory in $CreatedDirectoryLocation ..." -ForegroundColor Green
Write-Host ''
Write-Host "Changing into cloned repository directory $ClonedRepoDirectoryLocation ..." -ForegroundColor Magenta
Set-Location $ClonedRepoDirectoryLocation -ErrorAction stop

# Check is user is logged in to Azure
$UserLoggedIntoAzure = Get-AzContext -ErrorAction SilentlyContinue
if ($null -eq $UserLoggedIntoAzure) {
  throw 'You are not logged into Azure. Please log into Azure using the Azure PowerShell module using the command of `Connect-AzAccount` to the correct tenant and try again.'
}
$UserLoggedIntoAzureJson = $UserLoggedIntoAzure | ConvertTo-Json -Depth 10 | ConvertFrom-Json
Write-Host "You are logged into Azure as '$($UserLoggedIntoAzureJson.Account.Id)' ..." -ForegroundColor Green

# Check user has access to desired subscription
$UserCanAccessSubscription = Get-AzSubscription -SubscriptionId $GitHubSecret_ARM_SUBSCRIPTION_ID -ErrorAction SilentlyContinue
if ($null -eq $UserCanAccessSubscription) {
  throw "You do not have access to the subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)'. Please ensure you have access to the subscription and try again."
}
Write-Host "You have access to the subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)' ..." -ForegroundColor Green
Write-Host ''

# Get GitHub Login/Org Name
$GitHubUserRaw = gh api user
$GitHubUserConvertedToJson = $GitHubUserRaw | ConvertFrom-Json -Depth 10
$GitHubOrgName = $GitHubUserConvertedToJson.login
$GitHubOrgAndRepoNameCombined = "$($GitHubOrgName)/bicep-registry-modules"

# Create SPN if not using OIDC
if ($UseOIDC -eq $false) {
  if ($SPNName -eq '') {
    Write-Host "No value provided for the SPN Name. Defaulting to 'spn-avm-bicep-brm-fork-ci-<GitHub Organization>' ..." -ForegroundColor Yellow

    $SPNName = "spn-avm-bicep-brm-fork-ci-$($GitHubOrgName)"
  }
  $newSpn = New-AzADServicePrincipal -DisplayName $SPNName -Description "Service Principal Name (SPN) for the AVM Bicep CI Tests in the $($GitHubOrgName) fork. See: https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/#2-configure-a-deployment-identity-in-azure" -ErrorAction Stop
  Write-Host "New SPN created with a Display Name of '$($newSpn.DisplayName)' and an Object ID of '$($newSpn.Id)'." -ForegroundColor Green
  Write-Host ''

  # Create RBAC Role Assignments for SPN
  Write-Host 'Starting 120 second sleep to allow the SPN to be created and available for RBAC Role Assignments (eventual consistency) ...' -ForegroundColor Yellow
  Start-Sleep -Seconds 120

  Write-Host "Creating RBAC Role Assignments of 'Owner' for the Service Principal Name (SPN) '$($newSpn.DisplayName)' on the Subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)' ..." -ForegroundColor Magenta
  New-AzRoleAssignment -ApplicationId $newSpn.AppId -RoleDefinitionName 'Owner' -Scope "/subscriptions/$($GitHubSecret_ARM_SUBSCRIPTION_ID)" -ErrorAction Stop
  Write-Host "RBAC Role Assignments of 'Owner' for the Service Principal Name (SPN) '$($newSpn.DisplayName)' created successfully on the Subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)'." -ForegroundColor Green
  Write-Host ''

  if ($GitHubSecret_ARM_MGMTGROUP_ID -eq '') {
    Write-Host "No Management Group ID provided as input parameter to '-GitHubSecret_ARM_MGMTGROUP_ID', skipping RBAC Role Assignments upon Management Groups" -ForegroundColor Yellow
    Write-Host ''
  }

  if ($GitHubSecret_ARM_MGMTGROUP_ID -ne '') {
    Write-Host "Creating RBAC Role Assignments of 'Owner' for the Service Principal Name (SPN) '$($newSpn.DisplayName)' on the Management Group with the ID of '$($GitHubSecret_ARM_MGMTGROUP_ID)' ..." -ForegroundColor Magenta
    New-AzRoleAssignment -ApplicationId $newSpn.AppId -RoleDefinitionName 'Owner' -Scope "/providers/Microsoft.Management/managementGroups/$($GitHubSecret_ARM_MGMTGROUP_ID)" -ErrorAction Stop
    Write-Host "RBAC Role Assignments of 'Owner' for the Service Principal Name (SPN) '$($newSpn.DisplayName)' created successfully on the Management Group with the ID of '$($GitHubSecret_ARM_MGMTGROUP_ID)'." -ForegroundColor Green
    Write-Host ''
  }
}

# Create UAMI if using OIDC
if ($UseOIDC) {
  if ($UAMIName -eq '') {
    Write-Host "No value provided for the UAMI Name. Defaulting to 'id-avm-bicep-brm-fork-ci-<GitHub Organization>' ..." -ForegroundColor Yellow

    $UAMIName = "id-avm-bicep-brm-fork-ci-$($GitHubOrgName)"
  }
  if ($UAMIRsgName -eq '') {
    Write-Host "No value provided for the UAMI Resource Group Name. Defaulting to 'rsg-avm-bicep-brm-fork-ci-<GitHub Organization>-oidc' ..." -ForegroundColor Yellow

    $UAMIRsgName = "rsg-avm-bicep-brm-fork-ci-$($GitHubOrgName)-oidc"
  }

  Write-Host "Selecting the subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)' to create Resource Group & UAMI in for OIDC ..." -ForegroundColor Magenta
  Select-AzSubscription -Subscription $GitHubSecret_ARM_SUBSCRIPTION_ID
  Write-Host ''

  if ($UAMIRsgLocation -eq '') {
    Write-Host "No value provided for the UAMI Location ..." -ForegroundColor Yellow
    $UAMIRsgLocation = Read-Host -Prompt "Please enter the location for the UAMI and the Resource Group to be created in for OIDC deployments. e.g. 'uksouth' or 'eastus', etc..."
    $UAMIRsgLocation = $UAMIRsgLocation.ToLower()

    $availableLocations = Get-AzLocation | Where-Object {$_.RegionType -eq 'Physical'} | Select-Object -ExpandProperty Location

    if ($availableLocations -notcontains $UAMIRsgLocation) {
      Write-Host "Invalid location provided. Please provide a valid location from the list below ..." -ForegroundColor Yellow
      Write-Host ''
      Write-Host "Available Locations: $($availableLocations -join ', ')" -ForegroundColor Yellow
      do {
        $UAMIRsgLocation = Read-Host -Prompt "Please enter the location for the UAMI and the Resource Group to be created in for OIDC deployments. e.g. 'uksouth' or 'eastus', etc..."
      } until (
        $availableLocations -icontains $UAMIRsgLocation
      )
    }
  }

  Write-Host "Creating Resource Group for UAMI with the name of '$($UAMIRsgName)' and location of '$($UAMIRsgLocation)'..." -ForegroundColor Magenta
  $newUAMIRsg = New-AzResourceGroup -Name $UAMIRsgName -Location $UAMIRsgLocation -ErrorAction Stop
  Write-Host "New Resource Group created with a Name of '$($newUAMIRsg.ResourceGroupName)' and a Location of '$($newUAMIRsg.Location)'." -ForegroundColor Green
  Write-Host ''

  Write-Host "Creating UAMI with the name of '$($UAMIName)' and location of '$($UAMIRsgLocation)' in the Resource Group with the name of '$($UAMIRsgName)..." -ForegroundColor Magenta
  $newUAMI = New-AzUserAssignedIdentity -ResourceGroupName $newUAMIRsg.ResourceGroupName -Name $UAMIName -Location $newUAMIRsg.Location -ErrorAction Stop
  Write-Host "New UAMI created with a Name of '$($newUAMI.Name)' and an Object ID of '$($newUAMI.PrincipalId)'." -ForegroundColor Green
  Write-Host ''

  # Create Federated Credentials for UAMI for OIDC
  Write-Host "Creating Federated Credentials for the User-Assigned Managed Identity Name (UAMI) for OIDC ... '$($newUAMI.Name)' for OIDC ..." -ForegroundColor Magenta
  New-AzFederatedIdentityCredentials -ResourceGroupName $newUAMIRsg.ResourceGroupName -IdentityName $newUAMI.Name -Name 'avm-gh-env-validation' -Issuer "https://token.actions.githubusercontent.com" -Subject "repo:$($GitHubOrgAndRepoNameCombined):environment:avm-validation" -ErrorAction Stop
  Write-Host ''

  # Create RBAC Role Assignments for UAMI
  Write-Host 'Starting 120 second sleep to allow the UAMI to be created and available for RBAC Role Assignments (eventual consistency) ...' -ForegroundColor Yellow
  Start-Sleep -Seconds 120

  Write-Host "Creating RBAC Role Assignments of 'Owner' for the User-Assigned Managed Identity Name (UAMI) '$($newUAMI.Name)' on the Subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)' ..." -ForegroundColor Magenta
  New-AzRoleAssignment -ObjectId $newUAMI.PrincipalId -RoleDefinitionName 'Owner' -Scope "/subscriptions/$($GitHubSecret_ARM_SUBSCRIPTION_ID)" -ErrorAction Stop
  Write-Host "RBAC Role Assignments of 'Owner' for the User-Assigned Managed Identity Name (UAMI) '$($newUAMI.Name)' created successfully on the Subscription with the ID of '$($GitHubSecret_ARM_SUBSCRIPTION_ID)'." -ForegroundColor Green
  Write-Host ''

  if ($GitHubSecret_ARM_MGMTGROUP_ID -eq '') {
    Write-Host "No Management Group ID provided as input parameter to '-GitHubSecret_ARM_MGMTGROUP_ID', skipping RBAC Role Assignments upon Management Groups" -ForegroundColor Yellow
    Write-Host ''
  }

  if ($GitHubSecret_ARM_MGMTGROUP_ID -ne '') {
    Write-Host "Creating RBAC Role Assignments of 'Owner' for the User-Assigned Managed Identity Name (UAMI) '$($newSpn.DisplayName)' on the Management Group with the ID of '$($GitHubSecret_ARM_MGMTGROUP_ID)' ..." -ForegroundColor Magenta
    New-AzRoleAssignment -ObjectId $newUAMI.PrincipalId -RoleDefinitionName 'Owner' -Scope "/providers/Microsoft.Management/managementGroups/$($GitHubSecret_ARM_MGMTGROUP_ID)" -ErrorAction Stop
    Write-Host "RBAC Role Assignments of 'Owner' for the User-Assigned Managed Identity Name (UAMI) '$($newUAMI.Name)' created successfully on the Management Group with the ID of '$($GitHubSecret_ARM_MGMTGROUP_ID)'." -ForegroundColor Green
    Write-Host ''
  }
}

# Set GitHub Repo Secrets (non-OIDC)
if ($UseOIDC -eq $false) {
  Write-Host "Setting GitHub Secrets on forked repository (non-OIDC) '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Magenta
  Write-Host 'Creating and formatting secret `AZURE_CREDENTIALS` with details from SPN creation process (non-OIDC) and other parameter inputs ...' -ForegroundColor Cyan

  $FormattedAzureCredentialsSecret = "{ 'clientId': '$($newSpn.AppId)', 'clientSecret': '$($newSpn.PasswordCredentials.SecretText)', 'subscriptionId': '$($GitHubSecret_ARM_SUBSCRIPTION_ID)', 'tenantId': '$($GitHubSecret_ARM_TENANT_ID)' }"
  $FormattedAzureCredentialsSecretJsonCompressed = $FormattedAzureCredentialsSecret | ConvertFrom-Json | ConvertTo-Json -Compress

  if ($GitHubSecret_ARM_MGMTGROUP_ID -ne '') {
    gh secret set ARM_MGMTGROUP_ID --body $GitHubSecret_ARM_MGMTGROUP_ID -R $GitHubOrgAndRepoNameCombined
  }
  gh secret set ARM_SUBSCRIPTION_ID --body $GitHubSecret_ARM_SUBSCRIPTION_ID -R $GitHubOrgAndRepoNameCombined
  gh secret set ARM_TENANT_ID --body $GitHubSecret_ARM_TENANT_ID -R $GitHubOrgAndRepoNameCombined
  gh secret set AZURE_CREDENTIALS --body $FormattedAzureCredentialsSecretJsonCompressed -R $GitHubOrgAndRepoNameCombined
  gh secret set TOKEN_NAMEPREFIX --body $GitHubSecret_TOKEN_NAMEPREFIX -R $GitHubOrgAndRepoNameCombined

  Write-Host ''
  Write-Host "Successfully created and set GitHub Secrets (non-OIDC) on forked repository '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Green
  Write-Host ''
}

# Set GitHub Repo Secrets & Environment (OIDC)
if ($UseOIDC) {
  Write-Host "Setting GitHub Environment (avm-validation) and required Secrets on forked repository (OIDC) '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Magenta
  Write-Host "Creating 'avm-validation' environment on forked repository' ..." -ForegroundColor Cyan

  $GitHubEnvironment = gh api --method PUT -H "Accept: application/vnd.github+json" "repos/$($GitHubOrgAndRepoNameCombined)/environments/avm-validation"
  $GitHubEnvironmentConvertedToJson = $GitHubEnvironment | ConvertFrom-Json -Depth 10

  if ($GitHubEnvironmentConvertedToJson.name -ne 'avm-validation') {
    throw "Failed to create 'avm-validation' environment on forked repository. Please check the error message above, resolve any issues, and try again."
  }

  Write-Host "Successfully created 'avm-validation' environment on forked repository' ..." -ForegroundColor Green
  Write-Host ''

  Write-Host "Creating and formatting secrets for 'avm-validation' environment with details from UAMI creation process (OIDC) and other parameter inputs ..." -ForegroundColor Cyan
  gh secret set VALIDATE_CLIENT_ID --body $newUAMI.ClientId -R $GitHubOrgAndRepoNameCombined -e 'avm-validation'
  gh secret set VALIDATE_SUBSCRIPTION_ID --body $GitHubSecret_ARM_SUBSCRIPTION_ID -R $GitHubOrgAndRepoNameCombined -e 'avm-validation'
  gh secret set VALIDATE_TENANT_ID --body $GitHubSecret_ARM_TENANT_ID -R $GitHubOrgAndRepoNameCombined -e 'avm-validation'

  Write-Host "Creating and formatting secrets for repo with details from UAMI creation process (OIDC) and other parameter inputs ..." -ForegroundColor Cyan
  if ($GitHubSecret_ARM_MGMTGROUP_ID -ne '') {
    gh secret set ARM_MGMTGROUP_ID --body $GitHubSecret_ARM_MGMTGROUP_ID -R $GitHubOrgAndRepoNameCombined
  }
  gh secret set ARM_SUBSCRIPTION_ID --body $GitHubSecret_ARM_SUBSCRIPTION_ID -R $GitHubOrgAndRepoNameCombined
  gh secret set ARM_TENANT_ID --body $GitHubSecret_ARM_TENANT_ID -R $GitHubOrgAndRepoNameCombined
  gh secret set TOKEN_NAMEPREFIX --body $GitHubSecret_TOKEN_NAMEPREFIX -R $GitHubOrgAndRepoNameCombined

  Write-Host ''
  Write-Host "Successfully created and set GitHub Secrets in 'avm-validation' environment and repo (OIDC) on forked repository '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Green
  Write-Host ''
}

Write-Host "Opening browser so you can enable GitHub Actions on newly forked repository '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Magenta
Write-Host "Please select click on the green button stating 'I understand my workflows, go ahead and enable them' to enable actions/workflows on your forked repository via the website that has appeared in your browser window and then return to this terminal session to continue ..." -ForegroundColor Yellow
Start-Process "https://github.com/$($GitHubOrgAndRepoNameCombined)/actions" -ErrorAction Stop
Write-Host ''

$GitHubWorkflowPlatformToggleWorkflows = '.Platform - Toggle AVM workflows'
$GitHubWorkflowPlatformToggleWorkflowsFileName = 'platform.toggle-avm-workflows.yml'

do {
  Write-Host "Did you successfully enable the GitHub Actions/Workflows on your forked repository '$($GitHubOrgAndRepoNameCombined)'? Please enter 'y' or 'n'." -ForegroundColor Yellow
  $userInput = Read-Host
  $userInput = $userInput.ToLower()

  switch ($userInput) {
    'y' {
      Write-Host ''
      Write-Host "User Confirmed. Proceeding to trigger workflow of '$($GitHubWorkflowPlatformToggleWorkflows)' to disable all workflows as per: https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/enable-or-disable-workflows/..." -ForegroundColor Green
      Write-Host ''
      break
    }
    'n' {
      Write-Host ''
      Write-Host 'User stated no. Ending script here. Please review and complete any of the steps you have not completed, likely just enabling GitHub Actions/Workflows on your forked repository and then disabling all workflows as per: https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/enable-or-disable-workflows/' -ForegroundColor Yellow
      exit
    }
    default {
      Write-Host ''
      Write-Host "Invalid input. Please enter 'y' or 'n'." -ForegroundColor Red
      Write-Host ''
    }
  }
} while ($userInput -ne 'y' -and $userInput -ne 'n')

Write-Host "Setting Read/Write Workflow permissions on forked repository '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Magenta
gh api --method PUT -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "/repos/$($GitHubOrgAndRepoNameCombined)/actions/permissions/workflow" -f "default_workflow_permissions=write"
Write-Host ''

Write-Host "Triggering '$($GitHubWorkflowPlatformToggleWorkflows) on '$($GitHubOrgAndRepoNameCombined)' ..." -ForegroundColor Magenta
Write-Host ''
gh workflow run $GitHubWorkflowPlatformToggleWorkflows -R $GitHubOrgAndRepoNameCombined
Write-Host ''

Write-Host 'Starting 120 second sleep to allow the workflow run to complete ...' -ForegroundColor Yellow
Start-Sleep -Seconds 120
Write-Host ''

Write-Host "Workflow '$($GitHubWorkflowPlatformToggleWorkflows) on '$($GitHubOrgAndRepoNameCombined)' should have now completed, opening workflow in browser so you can check ..." -ForegroundColor Magenta
Start-Process "https://github.com/$($GitHubOrgAndRepoNameCombined)/actions/workflows/$($GitHubWorkflowPlatformToggleWorkflowsFileName)" -ErrorAction Stop
Write-Host ''

Write-Host "Script execution complete. Fork of '$($GitHubOrgAndRepoNameCombined)' created and configured and cloned to '$($ClonedRepoDirectoryLocation)' as per Bicep contribution guide: https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/ you are now ready to proceed from step 4. Opening the Bicep Contribution Guide for you to review and continue..." -ForegroundColor Green
Start-Process 'https://azure.github.io/Azure-Verified-Modules/contributing/bicep/bicep-contribution-flow/'

1. Fork the module source repository

Tip

Checkout the PowerShell Helper Script that can do this step automatically for you! πŸ‘


Note

Each time in the following sections we refer to ‘your xyz’, it is an indicator that you have to change something in your own environment.

Bicep AVM Modules (Resource, Pattern and Utility modules) are located in the /avm directory of the Azure/bicep-registry-modules repository, as per SNFR19.

Module owners are expected to fork the Azure/bicep-registry-modules repository and work on a branch from within their fork, before creating a Pull Request (PR) back into the Azure/bicep-registry-modules repository’s upstream main branch.

To do so, simply navigate to the Public Bicep Registry repository, select the 'Fork' button to the top right of the UI, select where the fork should be created (i.e., the owning organization) and finally click ‘Create fork’.

1.1 Create a GitHub environment

Create the avm-validation environment in your fork.

βž• How to: Create an environment in GitHub
  1. Navigate to the repository’s Settings.

  2. In the list of settings, expand Environments. You can create a new environment by selecting New environment on the top right.

  3. In the opening view, provide avm-validation for the environment Name. Click on the Configure environment button.

AddEnvironment AddEnvironment

Please ref the following link for additional details: Creating an environment

2. Configure a deployment identity in Azure

AVM tests its modules via deployments in an Azure subscription. To do so, it requires a deployment identity with access to it.

Deprecating the Service Principal + Secret authentication method

Support for the ‘Service Principal + Secret’ authentication method has been deprecated and will be decommissioned in the future.

It is highly recommended to start leveraging Option 1 below to adopt OpenID Connect (OIDC) authentication and align with security best practices.

βž• Option 1 [Recommended]: OIDC - Configure a federated identity credential
Using a Managed Identity for OIDC

Make sure to use a Managed Identity for OIDC as instructed below, not a Service Principal. Azure access token issued by Managed Identities is expected to have an expiration of 24 hours by default. With Service Principal, instead, it would be only 1 hour - which is not sufficient for many deployment pipelines.

For more information, please refer to the official GitHub documentation.

  1. Create a new or leverage an existing user-assigned managed identity with at least Contributor & User Access Administrator permissions on the Management-Group/Subscription you want to test the modules in. You may find creating an Owner role assignment is more efficient and avoids some validation failures for some modules. You might find the following links useful:

    OIDCIdentityRoles OIDCIdentityRoles

Additional roles

Some Azure resources may require additional roles to be assigned to the deployment identity. An example is the avm/res/aad/domain-service module, which requires the deployment identity to have the Domain Services Contributor Azure role to create the required Domain Services resources.

In those cases, for the first PR adding such modules to the public registry, we recommend the author to reach out to AVM maintainers or, alternatively, to create a CI environment GitHub issue in BRM, specifying the additional prerequisites. This ensures that the required additional roles get assigned in the upstream CI environment before the corresponding PR gets merged.

  1. Configure a federated identity credential on a user-assigned managed identity to trust tokens issued by GitHub Actions to your GitHub repository.
    • In the Microsoft Entra admin center, navigate to the user-assigned managed identity you created. Under Settings in the left nav bar, select Federated credentials and then Add Credential.
      OIDCFederatedCredentials OIDCFederatedCredentials
    • In the Federated credential scenario dropdown box, select GitHub Actions deploying Azure resources
      OIDCScenario OIDCScenario
    • For the Organization, specify your GitHub organization name, for the Repository the value bicep-registry-modules.
    • For the Entity type, select Environment and specify the value avm-validation.
    • Add a Name for the federated credential, for example, avm-gh-env-validation.
    • The Issuer, Audiences, and Subject identifier fields auto-populate based on the values you entered.
    • Select Add to configure the federated credential.
      OIDCAdd OIDCAdd
    • You might find the following links & information useful:
      • If configuring the federated credential via API (e.g. Bicep, PowerShell etc.), you will need the following information points that are configured automatically for you via the portal experience:
        • Issuer = https://token.actions.githubusercontent.com
        • Subject = repo:<GitHub Org>/<GitHub Repo>:environment:avm-validation
        • Audience = api://AzureADTokenExchange (although this is default in the API so not required to set)
      • Configure a federated identity credential on a user-assigned managed identity
  2. Note down the following pieces of information
    • Client ID
    • Tenant ID
    • Subscription ID
    • Parent Management Group ID
      OIDCInfo OIDCInfo

Additional references:

βž• Option 2 [Deprecated]: Configure Service Principal + Secret
  1. Create a new or leverage an existing Service Principal with at least Contributor & User Access Administrator permissions on the Management-Group/Subscription you want to test the modules in. You may find creating an Owner role assignment is more efficient and avoids some validation failures for some modules. You might find the following links useful:
  2. Note down the following pieces of information
    • Application (Client) ID
    • Service Principal Object ID (not the object ID of the application)
    • Service Principal Secret (password)
    • Tenant ID
    • Subscription ID
    • Parent Management Group ID

3. Configure your CI environment

Tip

Checkout the PowerShell Helper Script that can do this step automatically for you! πŸ‘

To configure the forked CI environment you have to perform several steps:

3.1. Set up secrets

3.1.1 Shared repository secrets

To use the Continuous Integration environment’s workflows you should set up the following repository secrets:

Secret NameExampleDescription
ARM_MGMTGROUP_ID11111111-1111-1111-1111-111111111111The group ID of the management group to test-deploy modules in. Is needed for resources that are deployed to the management group scope.
ARM_SUBSCRIPTION_ID22222222-2222-2222-2222-222222222222The ID of the subscription to test-deploy modules in. Is needed for resources that are deployed to the subscription scope. Note: This repository secret will be deprecated in favor of the VALIDATE_SUBSCRIPTION_ID environment secret required by the OIDC authentication.
ARM_TENANT_ID33333333-3333-3333-3333-333333333333The tenant ID of the Azure Active Directory tenant to test-deploy modules in. Is needed for resources that are deployed to the tenant scope. Note: This repository secret will be deprecated in favor of the VALIDATE_TENANT_ID environment secret required by the OIDC authentication.
TOKEN_NAMEPREFIXcntsoRequired. A short (3-5 character length), unique string that should be included in any deployment to Azure. Usually, AVM Bicep test cases require this value to ensure no two contributors deploy resources with the same name - which is especially important for resources that require a globally unique name (e.g., Key Vault). These characters will be used as part of each resource’s name during deployment. For more information, see the [Special case: TOKEN_NAMEPREFIX] note below.
Special case: TOKEN_NAMEPREFIX

To lower the barrier to entry and allow users to easily define their own naming conventions, we introduced a default ’name prefix’ for all deployed resources.

This prefix is only used by the CI environment you validate your modules in, and doesn’t affect the naming of any resources you deploy as part of any solutions (applications/workloads) based on the modules.

Each workflow in AVM deploying resources uses a logic that automatically replaces “tokens” (i.e., placeholders) in any module test file. These tokens are, for example, included in the resources names (e.g. 'name: kvlt-${namePrefix}'). Tokens are stored as repository secrets to facilitate maintenance.

βž• How to: Add a repository secret to GitHub
  1. Navigate to the repository’s Settings.

    NavigateToSettings NavigateToSettings

  2. In the list of settings, expand Secrets and select Actions. You can create a new repository secret by selecting New repository secret on the top right.

    NavigateToSecrets NavigateToSecrets

  3. In the opening view, you can create a secret by providing a secret Name, a secret Value, followed by a click on the Add secret button.

    AddSecret AddSecret

3.1.2 Authentication secrets

In addition to shared repository secrets detailed above, additional GitHub secrets are required to allow the deploying identity to authenticate to Azure.

Expand and follow the option corresponding to the deployment identity setup chosen at Step 2 and use the information you gathered during that step.

βž• Option 1 [Recommended]: Authenticate via OIDC

Create the following environment secrets in the avm-validation GitHub environment created at Step 1

Secret NameExampleDescription
VALIDATE_CLIENT_ID44444444-4444-4444-4444-444444444444The login credentials of the deployment principal used to log into the target Azure environment to test in. The format is described here.
VALIDATE_SUBSCRIPTION_ID22222222-2222-2222-2222-222222222222Same as the ARM_SUBSCRIPTION_ID repository secret set up above. The ID of the subscription to test-deploy modules in. Is needed for resources that are deployed to the subscription scope.
VALIDATE_TENANT_ID33333333-3333-3333-3333-333333333333Same as the ARM_TENANT_ID repository secret set up above. The tenant ID of the Azure Active Directory tenant to test-deploy modules in. Is needed for resources that are deployed to the tenant scope.
βž• How to: Add an environment secret to GitHub
  1. Navigate to the repository’s Settings.

    NavigateToSettings NavigateToSettings

  2. In the list of settings, select Environments. Click on the previously created avm-validation environment.

    NavigateToEnvironments NavigateToEnvironments

  3. In the Environment secrets Section click on the Add environment secret button.

    NavigateToEnvSecrets NavigateToEnvSecrets

  4. In the opening view, you can create a secret by providing a secret Name, a secret Value, followed by a click on the Add secret button.
    AddEnvSecret AddEnvSecret

βž• Option 2 [Deprecated]: Authenticate via Service Principal + Secret

Create the following environment repository secret:

Secret NameExampleDescription
AZURE_CREDENTIALS{"clientId": "44444444-4444-4444-4444-444444444444", "clientSecret": "<placeholder>", "subscriptionId": "22222222-2222-2222-2222-222222222222", "tenantId": "33333333-3333-3333-3333-333333333333" }The login credentials of the deployment principal used to log into the target Azure environment to test in. The format is described here. For more information, see the [Special case: AZURE_CREDENTIALS] note below.
Special case: AZURE_CREDENTIALS

This secret represent the service connection to Azure, and its value is a compressed JSON object that must match the following format:

{"clientId": "<client_id>", "clientSecret": "<client_secret>", "subscriptionId": "<subscriptionId>", "tenantId": "<tenant_id>" }

Make sure you create this object as one continuous string as shown above - using the information you collected during Step 2. Failing to format the secret as above, causes GitHub to consider each line of the JSON object as a separate secret string. If you’re interested, you can find more information about this object here.

3.2. Enable actions

Finally, ‘GitHub Actions’ are disabled by default and hence, must be enabled first.

To do so, perform the following steps:

  1. Navigate to the Actions tab on the top of the repository page.

  2. Next, select ‘I understand my workflows, go ahead and enable them’.

    EnableActions EnableActions

3.3. Set Read/Write Workflow permissions

To let the workflow engine publish their results into your repository, you have to enable the read / write access for the GitHub actions.

  1. Navigate to the Settings tab on the top of your repository page.

  2. Within the section Code and automation click on Actions and General

  3. Make sure to enable Read and write permissions

    WorkflowPermissions WorkflowPermissions

Tip

Once you enabled the GitHub actions, your workflows will behave as they do in the upstream repository. This includes a scheduled trigger to continuously check that all modules are working and compliant with the latest tests. However, testing all modules can incur substantial costs with the target subscription. Therefore, we recommend disabling all workflows of modules you are not working on. To make this as easy as possible, we created a workflow that disables/enables workflows based on a selected toggle & naming pattern. For more information on how to use this workflow, please refer to the corresponding documentation.

4. Implement your contribution

To implement your contribution, we kindly ask you to first review the Bicep specifications and composition guidelines in particular to make sure your contribution complies with the repository’s design and principles.

If you’re working on a new module, we’d also ask you to create its corresponding workflow file. Each module has its own file, but only differs in very few details, such as its triggers and pipeline variables. As a result, you can either copy & update any other module workflow file (starting with 'avm.[res|ptn|utl].') or leverage the following template:

βž• Module workflow template
# >>> UPDATE to for example "avm.res.key-vault.vault" and remove this comment
name: "avm.[res|ptn|utl].[provider-namespace].[resource-type]"

on:
  workflow_dispatch:
    inputs:
      staticValidation:
        type: boolean
        description: "Execute static validation"
        required: false
        default: true
      deploymentValidation:
        type: boolean
        description: "Execute deployment validation"
        required: false
        default: true
      removeDeployment:
        type: boolean
        description: "Remove deployed module"
        required: false
        default: true
      customLocation:
        type: string
        description: "Default location overwrite (e.g., eastus)"
        required: false
  push:
    branches:
      - main
    paths:
      - ".github/actions/templates/avm-**"
      - ".github/workflows/avm.template.module.yml"
        # >>> UPDATE to for example ".github/workflows/avm.res.key-vault.vault.yml" and remove this comment
      - ".github/workflows/avm.[res|ptn|utl].[provider-namespace].[resource-type].yml"
        # >>> UPDATE to for example "avm/res/key-vault/vault/**" and remove this comment
      - "avm/[res|ptn|utl]/[provider-namespace]/[resource-type]/**"
      - "utilities/pipelines/**"
      - "!utilities/pipelines/platform/**"
      - "!*/**/README.md"

env:
  # >>> UPDATE to for example "avm/res/key-vault/vault" and remove this comment
  modulePath: "avm/[res|ptn|utl]/[provider-namespace]/[resource-type]"
  # >>> Update to for example ".github/workflows/avm.res.key-vault.vault.yml" and remove this comment
  workflowPath: ".github/workflows/avm.[res|ptn|utl].[provider-namespace].[resource-type].yml"

concurrency:
  group: ${{ github.workflow }}

jobs:
  ###########################
  #   Initialize pipeline   #
  ###########################
  job_initialize_pipeline:
    runs-on: ubuntu-latest
    name: "Initialize pipeline"
    if: ${{ !cancelled() && !(github.repository != 'Azure/bicep-registry-modules' && github.event_name != 'workflow_dispatch') }}
    steps:
      - name: "Checkout"
        uses: actions/checkout@v5
        with:
          fetch-depth: 0
      - name: "Set input parameters to output variables"
        id: get-workflow-param
        uses: ./.github/actions/templates/avm-getWorkflowInput
        with:
          workflowPath: "${{ env.workflowPath}}"
      - name: "Get module test file paths"
        id: get-module-test-file-paths
        uses: ./.github/actions/templates/avm-getModuleTestFiles
        with:
          modulePath: "${{ env.modulePath }}"
    outputs:
      workflowInput: ${{ steps.get-workflow-param.outputs.workflowInput }}
      moduleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.moduleTestFilePaths }}
      psRuleModuleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.psRuleModuleTestFilePaths }}
      modulePath: "${{ env.modulePath }}"

  ##############################
  #   Call reusable workflow   #
  ##############################
  call-workflow-passing-data:
    name: "Run"
    permissions:
      id-token: write # For OIDC
      contents: write # For release tags
    needs:
      - job_initialize_pipeline
    uses: ./.github/workflows/avm.template.module.yml
    with:
      workflowInput: "${{ needs.job_initialize_pipeline.outputs.workflowInput }}"
      moduleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.moduleTestFilePaths }}"
      psRuleModuleTestFilePaths: "${{ needs.job_initialize_pipeline.outputs.psRuleModuleTestFilePaths }}"
      modulePath: "${{ needs.job_initialize_pipeline.outputs.modulePath}}"
    secrets: inherit
Note

The workflow is configured to be triggered by any changes in the main branch of Upstream (i.e., Azure/bicep-registry-modules) that could affect the module or its validation. However, in a fork, the workflow is stopped immediately after being triggered due to the condition:

# Only run if not canceled and not in a fork, unless triggered by a workflow_dispatch event
if: ${{ !cancelled() && !(github.repository != 'Azure/bicep-registry-modules' && github.event_name != 'workflow_dispatch') }}

This condition prevents accidentally triggering a large amount of module workflows, e.g., when merging upstream changes into your fork.

In forks, workflow validation remains possible through explicit runs (that is, by using the Β workflow_dispatchΒ  event).

Tip

After any change to a module and before running tests, we highly recommend running the Set-AVMModule utility to update all module files that are auto-generated (e.g., the main.json & readme.md files).

5. Create/Update and run tests

Before opening a Pull Request to the Bicep Public Registry, ensure your module is ready for publishing, by validating that it meets all the Testing Specifications as per SNFR1, SNFR2, SNFR3, SNFR4, SNFR5, SNFR6, SNFR7.

For example, to meet SNFR2, ensure the updated module is deployable against a testing Azure subscription and compliant with the intended configuration.

Depending on the type of contribution you implemented (for example, a new resource module feature) we would kindly ask you to also update the e2e test run by the pipeline. For a new parameter this could mean to either add its usage to an existing test file, or to add an entirely new test as per BCPRMNFR1.

Once the contribution is implemented and the changes are pushed to your forked repository, we kindly ask you to validate your updates in your own cloud environment before requesting to merge them to the main repo. Test your code leveraging the forked AVM CI environment you configured before

Tip

In case your contribution involves changes to a module, you can also optionally leverage the Validate module locally utility to validate the updated module from your local host before validating it through its pipeline.

Creating end-to-end tests

As per BCPRMNFR1, a resource module must contain a minimum set of deployment test cases, while for pattern modules there is no restriction on the naming each deployment test must have.
In either case, you’re free to implement any additional, meaningful test that you see fit. Each test is implemented in its own test folder, containing at least a main.test.bicep and optionally any amount of extra deployment files that you may require (e.g., to deploy dependencies using a dependencies.bicep that you reference in the test template file).

To get started implementing your test in the main.test.bicep file, we recommend the following guidelines:

  • As per BCPNFR13, each main.test.bicep file should implement metadata to render the test more meaningful in the documentation

  • The main.test.bicep file should deploy any immediate dependencies (e.g., a resource group, if required) and invoke the module’s main template while providing all parameters for a given test scenario.

  • Parameters

    • Each file should define a parameter serviceShort. This parameter should be unique to this file (i.e, no two test files should share the same) as it is injected into all resource deployments, making them unique too and account for corresponding requirements.

      • As a reference you can create a identifier by combining a substring of the resource type and test scenario (e.g., in case of a Linux Virtual Machine Deployment: vmlin).

      • For the substring, we recommend to take the first character and subsequent ‘first’ character from the resource type identifier and combine them into one string. Following you can find a few examples for reference:

        • db-for-postgre-sql/flexible-server with a test folder default could be: dfpsfsdef
        • storage/storage-account with a test folder waf-aligned could be: ssawaf

        πŸ’‘ If the combination of the servicesShort with the rest of a resource name becomes too long, it may be necessary to bend the above recommendations and shorten the name.
        This can especially happen when deploying resources such as Virtual Machines or Storage Accounts that only allow comparatively short names.

    • If the module deploys a resource-group-level resource, the template should further have a resourceGroupName parameter and subsequent resource deployment. As a reference for the default name you can use dep-<namePrefix><providerNamespace>.<resourceType>-${serviceShort}-rg.

    • Each file should also provide a location parameter that may default to the deployments default location

  • It is recommended to define all major resource names in the main.test.bicep file as it makes later maintenance easier. To implement this, make sure to pass all resource names to any referenced module (including any resource deployed in the dependencies.bicep).

  • Further, for any test file (including the dependencies.bicep file), the usage of variables should be reduced to the absolute minimum. In other words: You should only use variables if you must use them in more than one place. The idea is to keep the test files as simple as possible

  • References to dependencies should be implemented using resource references in combination with outputs. In other words: You should not hardcode any references into the module template’s deployment. Instead use references such as nestedDependencies.outputs.managedIdentityPrincipalId

    Important

    As per BCPNFR12 you must use the header module testDeployment '../.*main.bicep' = when invoking the module’s template.

    Tip

Dependency file (dependencies.bicep) guidelines:

  • The dependencies.bicep should optionally be used if any additional dependencies must be deployed into a nested scope (e.g. into a deployed Resource Group).

  • Note that you can reuse many of the assets implemented in other modules. For example, there are many recurring implementations for Managed Identities, Key Vaults, Virtual Network deployments, etc.

  • A special case to point out is the implementation of Key Vaults that require purge protection (for example, for Customer Managed Keys). As this implies that we cannot fully clean up a test deployment, it is recommended to generate a new name for this resource upon each pipeline run using the output of the utcNow() function at the time.

    Tip
    Tip

    πŸ“œ If your test case requires any value that you cannot / should not specify in the test file itself (e.g., tenant-specific object IDs or secrets), please refer to the Custom CI secrets feature.

Reusable assets

There are a number of additional scripts and utilities available here that may be of use to module owners/contributors. These contain both scripts and Bicep templates that you can re-use in your test files (e.g., to deploy standadized dependencies, or to generate keys using deployment scripts).

Example: Certificate creation script

If you need a Deployment Script to set additional non-template resources up (for example certificates/files, etc.), we recommend to store it as a file in the shared utilities/e2e-template-assets/scripts folder and load it using the template function loadTextContent() (for example: scriptContent: loadTextContent('../../../../../../utilities/e2e-template-assets/scripts/New-SSHKey.ps1')). This approach makes it easier to test & validate the logic and further allows reusing the same logic across multiple test cases.

Example: Diagnostic Settings dependencies

To test the numerous diagnostic settings targets (Log Analytics Workspace, Storage Account, Event Hub, etc.) the AVM core team have provided a dependencies .bicep file to help create all these pre-requisite targets that will be needed during test runs.

βž• Diagnostic Settings Dependencies - Bicep File
// ========== //
// Parameters //
// ========== //

@description('Required. The name of the storage account to create.')
@maxLength(24)
param storageAccountName string

@description('Required. The name of the log analytics workspace to create.')
param logAnalyticsWorkspaceName string

@description('Required. The name of the event hub namespace to create.')
param eventHubNamespaceName string

@description('Required. The name of the event hub to create inside the event hub namespace.')
param eventHubNamespaceEventHubName string

@description('Optional. The location to deploy resources to.')
param location string = resourceGroup().location

// ============ //
// Dependencies //
// ============ //

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-08-01' = {
  name: storageAccountName
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_LRS'
  }
  properties: {
    allowBlobPublicAccess: false
  }
}

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
  name: logAnalyticsWorkspaceName
  location: location
}

resource eventHubNamespace 'Microsoft.EventHub/namespaces@2021-11-01' = {
  name: eventHubNamespaceName
  location: location

  resource eventHub 'eventhubs@2021-11-01' = {
    name: eventHubNamespaceEventHubName
  }

  resource authorizationRule 'authorizationRules@2021-06-01-preview' = {
    name: 'RootManageSharedAccessKey'
    properties: {
      rights: [
        'Listen'
        'Manage'
        'Send'
      ]
    }
  }
}

// ======= //
// Outputs //
// ======= //

@description('The resource ID of the created Storage Account.')
output storageAccountResourceId string = storageAccount.id

@description('The resource ID of the created Log Analytics Workspace.')
output logAnalyticsWorkspaceResourceId string = logAnalyticsWorkspace.id

@description('The resource ID of the created Event Hub Namespace.')
output eventHubNamespaceResourceId string = eventHubNamespace.id

@description('The resource ID of the created Event Hub Namespace Authorization Rule.')
output eventHubAuthorizationRuleId string = eventHubNamespace::authorizationRule.id

@description('The name of the created Event Hub Namespace Event Hub.')
output eventHubNamespaceEventHubName string = eventHubNamespace::eventHub.name

6. Create a Pull Request to the Public Bicep Registry

Finally, once you are satisfied with your contribution and validated it, open a PR for the module owners or core team to review. Make sure you:

  1. Provide a meaningful title in the form of feat: <module name> to align with the Semantic PR Check.

  2. Provide a meaningful description.

  3. Follow instructions you find in the PR template.

  4. If applicable (i.e., a module is created/updated), please reference the badge status of your pipeline run. This badge will show the reviewer that the code changes were successfully validated & tested in your environment. To create a badge, first select the three dots (...) at the top right of the pipeline, and then chose the Create status badge option.

    BadgeDropdown BadgeDropdown

  5. In the opening pop-up, you first need to select your branch and then click on the Copy status badge Markdown

    StatusBadge StatusBadge

Note

If you receive any comments for your pull request, please adhere to the following practices

  • If it is a ‘suggestion’ that you agree with, you can directly commit it into your branch by selecting the ‘Apply suggestion’ button, auto-resolving the comment
  • If it’s a regular comment that you agree with, please address its ask and leave a comment indicating the same. Do not resolve it yourself as this renders a re-review a lot harder for the reviewer.
Note

If you’re the sole owner of the module, the AVM core team must review and approve the PR. To indicate that your PR needs the core team’s attention, apply the Β Needs: Core Team 🧞  label on it!

7. Get your pull request approved

To publish a new module or a new version of an existing module, each Pull Request (PR) MUST be reviewed and approved before being merged and published in the Public Bicep Registry. A contributor (the submitter of the PR) cannot approve their own PR.

This behavior is assisted by policies, bots, through automatic assignment of the expected reviewer(s) and supporting labels.

Important

As part of the PR review process, the submitter (contributor) MUST address any comments raised by the reviewers and request a new review - and repeat this process until the PR is approved.
Once the PR is merged, the module owner MUST ensure that the related GitHub Actions workflow has successfully published the new version of the module.

7.1. Publishing a new module

When publishing a net new module for the first time ever, the PR MUST be reviewed and approved by a member of the core team.

7.2. Publishing a new version of an existing module

When publishing a new version of an existing module (i.e., anything that is not being published for the first time ever), the PR approval logic is the following:

PR is submitted by a module ownerPR is submitted by anyone, other than the module owner
Module has a single module ownerAVM core team or in case of Terraform only, the owner of another module approves the PRModule owner approves the PR
Module has multiple module ownersAnother owner of the module (other than the submitter) approves the PROne of the owners of the module approves the PR

In case of Bicep modules, if the PR includes any changes outside of the “modules/” folder, it first needs the module related code changes need to be reviewed and approved as per the above table, and only then does the PR need to be approved by a member of the core team. This way the core team’s approval does not act as a bypass from the actual code review perspective.

Subsections of Contribution Flow

Child Module Publishing

Child resources are resources that exist only within the scope of another resource. For example, a virtual network subnet cannot exist without a virtual network. The subnet is a child resource of the virtual network.

In the context of AVM, particularly AVM Bicep resource modules, child modules are modules deploying child resources. They are implemented within the scope of their corresponding parent resource modules. For example, the module avm/res/network/virtual-network/subnet deploys a virtual network subnet and is a child module of its parent virtual network module avm/res/network/virtual-network.

By default, child modules are not published to the public bicep registry independently from their parents. They need to be explicitly enabled for publishing to be directly referenced from the registry.

This page covers step-by-step guidelines to publish a bicep child module.

Important

The child module publishing process is currently in a pilot/preview phase. This means it may not be as smooth as the general module publishing.

The core team is currently working on additional automation, with the goal of improving efficiency in addressing child module publishing requests.

Note

Child module publishing currently only applies to resource modules.

Supporting child module publishing for other module categories, such as pattern and utility modules, is not planned at this time.

Quick guide

Use this section for a fast overview on how to publish a child module.
For a step-by-step explanation with detailed instructions, refer to the following sections.

  • Check prerequisites: Existing issue in AVM, telemetry ID prefix assigned in Bicep Module Index CSV, module registered in the MAR-file.
  • Implement required changes in your fork:
    • Allowed list: If not present, add child module to child-module-publish-allowed-list.json.
    • Child module template: Add enableTelemetry parameter and avmTelemetry deployment to child main.bicep template.
    • Parent module template: In the main.bicep template of the child module direct parent, add a enableReferencedModulesTelemetry variable with a value of false, and pass it as the enableTelemetry value down to the child module deployment.
    • Version: Add the version.json file to the child module folder and set version to 0.1.
    • Changelog: Add a new CHANGELOG.md file to the child module folder and update the changelog of all its versioned parents with a new patch version, up to the top-level parent.
    • Set-AVMModule: Run the Set-AVMModule utility explicitly on the affected bicep modules, i.e., the child module(s) and all their parent modules up to the top-level module, test your changes and raise a PR.

Prerequisites

Before jumping into the implementation, make sure the following prerequisites are in place:

Bicep Child Module Proposal issue

Ensure there is an AVM issue open already, proposing the child module to be published. If not, please create one using the Bicep Child Module Proposal issue template.

Note

Please understand the difference between publishing an existing child module and extending a parent module with a not yet implemented child module functionality.

The Bicep Child Module Proposal issue primarily intends to cover the former, i.e. to publish a child module already existing in the BRM (Bicep Registry Modules) repository source code. However, the same issue allows also to request the development of the child module functionality, although the best way to address new functionality is to raise a feature request via the the AVM Module issue.

Telemetry ID prefix assigned

Follow the below steps to check the child module telemetry ID prefix.

Note

If the Bicep Child Module Proposal issue was just created, please allow a few days for the telemetry ID prefix to be assigned before reaching out.

  1. Check the online Bicep resource module index source CSV file here.
  2. Search for the child module name in the ModuleName field.
  3. Verify if the corresponding value exists in the TelemetryIdPrefix field. Note down the value as you will need it in the implementation phase.
  4. If not found, please reach out to the core team, mentioning the @Azure/avm-core-team-technical-bicep via the Bicep Child Module Proposal issue.

Module registered in the MAR-file

Ensure that the child module is registered in the MAR-file.
If not, please reach out to the core team, mentioning the @Azure/avm-core-team-technical-bicep via the Bicep Child Module Proposal issue.

Note

The MAR-file can only be accessed by Microsoft FTEs. If you are missing access, please reach out to the parent module owner for help.

Implementation

The quickest way to get the child module published is to enable it yourself, contributing via a pull request to the BRM repository.

Note

Publishing a child module does not change the folder hierarchy of the parent and child modules. The child module remains in its existing location within the parent module’s folder structure. No files or folders need to be moved or reorganized.

Please follow the steps below:

  • Make sure the child module name is listed in the publishing allowed list child-module-publish-allowed-list.json. If not, add it to the file, keeping an alphabetical order. This step is relevant until the process is in a pilot phase.

  • Update the child module main.bicep template to support telemetry, as per SFR4, SFR3 and BCPFR4

    • Add the enableTelemetry parameter with a default value of true. Place it as the last param declaration, immediately before the first var declaration.
      @description('Optional. Enable/Disable usage telemetry for module.')
      param enableTelemetry bool = true
    • Add the avmTelemetry deployment, referencing below template. Make sure to replace the <ReplaceWith-TelemetryIdPrefix> placeholder with the assigned telemetry ID prefix value that you noted down when checking prerequisites.
        #disable-next-line no-deployments-resources
        resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) {
          name: '<ReplaceWith-TelemetryIdPrefix>.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name), 0, 4)}'
          properties: {
            mode: 'Incremental'
            template: {
              '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
              contentVersion: '1.0.0.0'
              resources: []
              outputs: {
                telemetry: {
                  type: 'String'
                  value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
                }
              }
            }
          }
        }
  • Update the main.bicep template of the child module direct parent, as per BCPFR7.

    • Add the enableReferencedModulesTelemetry variable with a default value of false. Place it as the last var declaration, immediately before the first resource declaration.
      var enableReferencedModulesTelemetry = false
    • Pass the enableReferencedModulesTelemetry variable as the enableTelemetry value down to the child module deployment.
      enableTelemetry: enableReferencedModulesTelemetry
  • Add the version.json file to the child module folder and set version to 0.1.

    {
      "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
      "version": "0.1"
    }
  • Determine all affected module paths. This includes the child module(s) and all parent modules up to the top-level module.

    Examples:

    • Child module to publish: avm/res/virtual-network/subnet

    • Affected modules:

      • avm/res/virtual-network/subnet
      • avm/res/virtual-network
    • Child module to publish: avm/res/storage/storage-account/blob-service/container/immutability-policy

    • Affected modules:

      • avm/res/storage/storage-account/blob-service/container/immutability-policy
      • avm/res/storage/storage-account/blob-service/container
      • avm/res/storage/storage-account/blob-service
      • avm/res/storage/storage-account
    Retrieve affected module paths programmatically

    To retrieve affected module paths programmatically you can leverage the Get-ParentFolderPathList utility using the 'OnlyModules' option

    $affectedModulePathList = Get-ParentFolderPathList -Path '<path/to/avm/child-module>' -Filter 'OnlyModules'
  • Update Changelogs

    • Add a new CHANGELOG.md file to the child module folder, with the following sample content. Make sure to replace the <avm/res/path/to/child-module> placeholder with the name of the child module.
      # Changelog
      
      The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/<avm/res/path/to/child-module>/CHANGELOG.md).
      
      ## 0.1.0
      
      ### Changes
      
      - Initial version
      
      ### Breaking Changes
      
      - None
    • Check the list of affected modules. Update the changelog of all the affected modules with a version.json. Add a new patch version for each. Refer below for an example content section:
        ## <CurrentMajor>.<CurrentMinor>.<CurrentPatch+1>
      
        ### Changes
      
        - Publishing child module `<avm/res/path/to/child-module>`
      
        ### Breaking Changes
      
        - None
  • Run the Set-AVMModule utility, calling it explicitly on all affected modules.

    foreach ($modulePath in $affectedModulePaths) {
      Set-AVMModule -ModuleFolderPath $modulePath
    }
  • Test your changes via the top-level module pipeline, raise a PR and attach a status badge proving successful validation.

Note

Existing tests for the parent module do not need to be updated when publishing a child module. The changes required for child module publishing (telemetry support, version file, and changelogs) do not affect the module’s test cases.

Tip

Reference This pull request as an example for proposing a child module for publishing.

Custom CI Secrets

When working on a module, and more specifically its e2e deployment validation test cases, it may be necessary to leverage tenant-specific information such as:

  • Entra-ID-provided Enterprise Application object ids (e.g., Backup Management Service, Azure Databricks, etc.)
  • (sensitive) principal credentials (e.g., a custom service principal’s application id and secret)

The challenge with the former is that the value would be different from the contributor’s test tenant compared to the Upstream AVM one. This requires the contributor to temporarily change the value to their own tenant’s value during the contribution’s creation & testing, and for the reviewer to make sure the value is changed back before merging a PR in.
The challenge with the later is more critical as it would require the contributor to store sensitive information in source control and as such publish it.

To mitigate this challenge, the AVM CI provides you with the feature to store any such information in a custom Azure Key Vault and automatically pass it into your test cases in a dynamic & secure way.

Important

Since all modules must pass the tests in the AVM environment, it is important that you inform the maintainers when you add a new custom secret. The same secret must then also be set up in the upstream environment before the pull request is merged.

To make this matter not too complicated, we would like to ask you to emphasize this requirement in the description of your PR, for example by adding a text similar to:

- [ ] @avm-core-team-technical-bicep TODO: Add custom secret 'mySecret' to AVM CI

Example use case

Let’s assume you need a tenant-specific value like the object id of Azure’s Backup Management Service Enterprise Application for one of your tests. As you want to avoid hardcoding and consequently changing its value each time you want to contribute from your Fork to the main AVM repository, you want to instead have it be automatically pulled into your test cases.

To do so, you create a new parameter in your test case’s main.test.bicep file that you call, for example,

@secure()
param backupManagementServiceEnterpriseApplicationObjectId string = ''

assuming that it would be provided with the correct value by the AVM CI. You consequently reference it in your test case as you would with any other Bicep parameter.

Next, you create a new secret of the same name with a prefix CI- in a previously created Azure Key Vault of your test subscription (e.g., CI-backupManagementServiceEnterpriseApplicationObjectId). Its value would be the object id the Enterprise Application has in the tenant of your test subscription.

Assuming that also the CI_KEY_VAULT_NAME GitHub Repository variable is configured correctly, you can now run your test pipeline and observe how the CI automatically pulls the secret and passes it into your test cases, IF, they have a parameter with a matching name.

Setup

Pre-Requisites

To use this feature, there are really only three prerequisites:

  1. Create an Azure Key Vault in your test subscription
  2. Grant the principal you use for testing in the CI at least `Key Vault Secrets User’ permissions on that Key Vault to enable it to pull secrets from it
  3. Configure the name of that Key Vault as a ‘Repository variable’ CI_KEY_VAULT_NAME in your Fork.

The above will enable the CI to identify your Key Vault, look for matching secrets in it, and pull their values as needed.

RequiredGitHubVariable RequiredGitHubVariable

Configuring a secret

Building upon the prerequisites you only have to implement two actions per value to dynamically populate them during deployment validation:

  1. Create a @secure() parameter in your test file (main.test.bicep) that you want to populate and use it as you see fit.

For example:

@description('Required. My parameter\'s description. This value is tenant-specific and must be stored in the CI Key Vault in a secret named \'CI-MySecret\'.')
@secure()
param mySecret string = ''
Important

It is mandatory to declare the parameter as secure() as Key Vault secrets will be pulled and passed into the deployment as SecureString values.

Also, it must have an empty default to be compatible with the PSRule scans that require a value for all parameters.

  1. Configure a secret of the same name, but with a CI- prefix and corresponding value in the Azure Key Vault you set up as per the prerequisites.

ExampleSecretsInKeyVault ExampleSecretsInKeyVault

How it works

Assuming you completed both the prerequisites & setup steps and triggered your module’s workflow, the CI will perform the following actions:

  1. When approaching the deployment validation steps, the workflow will lookup the CI_KEY_VAULT_NAME repository variable
  2. If it has a value, it will subsequently pull all available secret references (not their values!) from that Key Vault, filtered down to only the secrets that match the CI- prefix
  3. It will then loop through these secret references and check if any match a parameter in the targeted test.main.bicep of the same name, but without the CI- prefix
  4. Only for a match, the workflow with then pull the secret from the Key Vault and pass its value as a SecureString as a parameter into the template deployment.

When reviewing the log during or after a run, you can see each matching and pulled secret is/was added as part of the AdditionalParameters object as seen in the following:

ExamplePipelineLog ExamplePipelineLog

Background: Why not simply use GitHub secrets?

When reviewing the above, you may wonder why an Azure Key Vault was used as opposed to simple GitHub secrets.

While the simplicity of GitHub secrets would be preferred, it unfortunately turned out that they would not provide us with the level of flexibility we need for our purposes.

Most notably, GitHub secrets are not automatically available in referenced GitHub actions. Instead, you have to declare every secret you want to use explicitly in the workflow’s template, requiring the contributor to update both the module’s workflow template as well as test files each time a new value would be added.
This characteristic is not only unfortunate for our use case, but is also a lot more likely to lead to mistakes.

Further, with the use of OIDC via Managed Identities, the hurdle to bootstrap & populate an Azure Key Vault is significantly lowered.

Enable or Disable Workflows

When forking the BRM repository, all workflows from the CI environment are also part of your fork. In an earlier step it was explained, how to set them up correctly, to verify your module development.

Due to the trigger mechanism of the workflows, eventually all of them run at some point in time, creating and deleting resources on Azure in your environment. That will also happen for modules, you are not working on. This will create costs in your own subscription and it can also create a queue for workflow runs, due to the lack of enough free agents.

To limit those workflow runs, you can manually disable each pipeline you do not want to run. As this is a time consuming task, there is script in the BRM repository, to disable (or enable) pipelines in a batch process, that can also be run via a workflow. You can also use RegEx to specify which pipelines should be included and which should be excluded.

Location

You can find the script under utilities/pipelines/platform/Switch-WorkflowState.ps1)
You can find the workflow under .github/workflows/platform.toggle-avm-workflows.yml

How it works

Browse to Actions and select the workflow from the list

SelectToggleWorkflows SelectToggleWorkflows

Run the workflow platform.toggle-avm-workflows and set the following settings:

  • Enable or disable workflows to enable or disable workflows
  • RegEx which workflows are included include a specific set of workflows, using a RegEx.
  • RegEx which workflows are excluded exclude a specific set of workflows, using a RegEx.

RunToggleWorkflows RunToggleWorkflows

Typical use cases

Disable all but one workflow

  • Enable or disable workflows to Disable
  • RegEx which workflows are included to avm\.(?:res|ptn|utl) (this is the default setting)
  • RegEx which workflows are excluded to avm.res.compute.virtual-machine (use the name of your own workflow. This example uses the workflow for virtual machine)

Disable all but multiple workflows

  • Enable or disable workflows to Disable
  • RegEx which workflows are included to avm\.(?:res|ptn|utl) (this is the default setting)
  • RegEx which workflows are excluded to (?:avm.res.compute.virtual-machine|avm.res.compute.image|avm.res.compute.disk) (use the names of your own workflows. This example uses the workflows for virtual machine, image, and disk)

Enable all workflows

  • Enable or disable workflows to Enable
  • RegEx which workflows are included to avm\.(?:res|ptn|utl) (this is the default setting)
  • RegEx which workflows are excluded to ^$ (this is the default setting)

Limitations

Please keep in mind, that the workflow run disables all workflows that match the RegEx at that point in time. If you sync your fork with the original repository and new workflows are there, they will be synced to your repository and will be enabled by default. So you will need to run the workflow to disable the new ones again after the sync.

Important

The workflow can only be triggered in forks.

Owner Contribution Flow

This section describes the contribution flow for module owners who are responsible for creating and maintaining Bicep Modules.

Important

This contribution flow is for Module Owners only.

As a Bicep Module Owner you need to be aware of the AVM Contribution Process Overview, Bicep specifications (including Bicep Interfaces) as these need to be followed during pull request reviews for the modules you own. The purpose of this Owner Contribution Flow is to simplify and list the most important activities of an owner and to help you understand your responsibilities as an owner.

Note

Additional internal content for ongoing module maintenance available for Microsoft FTEs, here.

1. Owner Activities and Responsibilities

Familiarise yourself with the responsibilities as Module Owner outlined in Team Definitions & RACI and Module Owner Responsibilities in the BRM Issue Triage.

  1. Create a GitHub team as outlined in SNFR20 and add it to the respective parent team:

    Naming convention:

    • avm-res-<RP>-<modulename>-module-owners-bicep

    Example:

    • avm-res-compute-virtualmachine-module-owners-bicep and added avm-technical-reviewers-bicep as parent.

    If a secondary or any additional owner is required, add them to the avm-res-<RP>-<modulename>-module-owners-bicep team.

    Only fulltime Microsoft employees can be added at this time.

    Info

    Once the team have been created the AVM Core Team will review the team name and parent team membership for accuracy. A notification will automatically be sent to the AVM Core Team to inform them that their review needs to be completed.

  2. Add the -owners- team to CODEOWNERS file as outlined in SNFR20.

  3. Ensure your module has been tested before raising a PR. You can do this your own or in another module contributor’s environment - if any. Also, once a PR is raised, a GitHub workflow pipeline is required to be run successfully before the PR can be merged. This is to ensure that the module is working as expected and is compliant with the AVM specifications.

    Note

    If you’re the sole owner of the module, the AVM core team must review and approve the PR. To indicate that your PR needs the core team’s attention, apply the Β Needs: Core Team 🧞  label on it!

  4. Ensure that the module(s) you own are compliant with the AVM Bicep specifications and are working as expected.

  5. Watch Pull Request (PR) activity for your module(s) in the BRM repository (Bicep Registry Modules repository - where all Bicep AVM modules are published) and ensure that PRs are reviewed and merged in a timely manner as outlined in SNFR11.

  6. Watch AVM module issue and AVM question/feedback activity for your module(s) in the BRM repository.

2. Module Handover Activities

Under certain circumstances, you may find yourself unable to continue as the module owner. In such cases, it is advisable to designate a new module owner. The following steps outline this transition:

  • Leave a comment on the original module proposal, indicating that you’d like to hand the ownership over to somebody else. Mention the person who originally helped triage the issue or the @Azure/avm-core-team-technical-bicep team. You must wait for someone from the AVM Core Team to respond first, as the module index must be updated before you can continue handing over the ownership.
  • Add the new owner’s GitHub account as a “maintainer” on your modules GitHub team.
  • Remove your GitHub account from your module’s GitHub team.

If a new module owner cannot be identified then the module will need to be “Orphaned”. Please follow the step outlined when-a-module-becomes-orphaned.

3. Adopting an Orphaned Module

When adopting an orphaned module the when-a-new-owner-is-identified steps must be followed.

4. GitHub Notification Settings

As a module owner, it’s important that you receive notifications when any of your AVM modules experience activity or when you or any groups you belong to are explicitly mentioned (using the @ operator). This document describes how to configure your GitHub and Email settings to ensure you receive email notifications for these types of scenarios within GitHub.

Enable Global GitHub Notifications

Visit the GitHub Notifications Settings Page while logged in with your GitHub account.

GitHubNotificationsSettingsPage GitHubNotificationsSettingsPage

  1. Ensure your Default Notifications Email address is set to the email address you intend to use.
  2. (Optional) If you would like to automatically watch repositories that you are active in, ensure Automatically watch repositories is set to “On.”
  3. (Required) If you would like to automatically subscribe to team-level notifications whenever you join a new team, ensure Automatically watch teams is set to “On.”
  4. (Required) To receive notifications whenever a change is made to a repository or conversation that you are Watching, ensure the Notify Me setting has at least Email enabled.
  5. (Required)To receive notifications whenever you or a group you belong to are @mentioned, ensure the Notify Me setting has at least Email enabled.

Watch a Repository

Optionally, you may consider “watching” (following most or all activities in) an entire repository. The primary repository that owners should watch is the Bicep-Registry-Modules (BRM) repository. Notifications from this repository will notify you of issues concerning your module and any direct or team @mentions. It is important that you read and react to these messages.

To watch the BRM repository, visit Bicep-Registry-Modules, click the Watch button in the top-right of the page, then select Participating and @mentions. Optionally, if you would like to be notified for all activity within the repository, you can select All Activity.

Note

Enabling All Activity will result in a lot of notifications! If you choose to go this route, you should set up filters within your email client. See Configure Email Inbox Notification Filters.

GitHubNotificationsPage GitHubNotificationsPage

Configure Email Inbox Notification Filters

GitHub uses a unique email address sender for each type of notification it sends. This allows us to set up filters within our email client to sort our inboxes depending on the type of notifications that was sent. The table below lists all of the relevant email addresses that may be useful for filtering notifications from GitHub.

Info

GitHub will use the following email addresses to Cc you if you’re subscribed to a conversation. The second Cc email address matches the notification reason.

Type of NotificationGitHub Email AddressNotification Reason
@Mentionsmention@noreply.github.comYou were mentioned on an issue or pull request.
@Team Mentionteam_mention@noreply.github.comA team you belong to was mentioned on an issue or pull request
Subscribedsubscribed@noreply.github.comThere was an update in a repository you’re watching.
Assignassign@noreply.github.comYou were assigned to an issue or pull request.
Commentcomment@noreply.github.comYou commented on an issue or pull request.

For a full list of GitHub notification types, see Filtering Email Notifications.

5. Contribution Checklist

This checklist can be used in the development of AVM Bicep Modules.

  1. Before beginning any work a new module a valid Issue: New AVM Module Proposal needs to be created. Instructions for creating the module proposal are outlined in the issue template. Pay particular attention to the questions and associated links to fill out the proposal accurately. Please do not start work on your proposed module until you receive a notification that your proposal has been accepted.

  2. Fork the bicep-registry-modules BRM repository. If you use an existing fork, ensure it’s up to date with origin/BRM.

    • Ensure all workflows are disabled by default once you forked the BRM repo, to prevent any accidental deployments into your Azure test environment resulted by an automated deployment.
  3. Create a new branch from your forked repository to develop your module.

  4. If you’re working on a new module you have to create its corresponding workflow file (see here).

    • In order to run your e2e tests in your fork, this workflow file has to be put into the main branch first, so it can be run against your feature branch (GitHub Workflows can only be run on feature branches when they are already present in the main branch).
    • Since all workflows are disabled by default you have to enable your module’s specific GitHub workflow to run your e2e tests.
  5. Implement your contribution.

  6. Create, update, and run tests.

    • In addition to testing your module via GitHub pipeline, you can also test-locally. The following helper script facilitates local testing.
    βž• Local Test Helper Script
    # Start pwsh if not started yet
    
    pwsh
    
    # Set default directory
    $folder = "<your directory>/bicep-registry-modules"
    
    # Dot source functions
    
    . $folder/utilities/tools/Set-AVMModule.ps1
    . $folder/utilities/tools/Test-ModuleLocally.ps1
    
    # Variables
    
    $modules = @(
        # "service-fabric/cluster", # Replace with your module
        "network/private-endpoint"  # Replace with your module
    )
    
    # Generate Readme
    
    foreach ($module in $modules) {
        Write-Output "Generating ReadMe for module $module"
        Set-AVMModule -ModuleFolderPath "$folder/avm/res/$module" -Recurse
    
        # Set up test settings
    
        $testcases = "waf-aligned", "max", "defaults"
    
        $TestModuleLocallyInput = @{
            TemplateFilePath           = "$folder/avm/res/$module/main.bicep"
            ModuleTestFilePath         = "$folder/avm/res/$module/tests/e2e/max/main.test.bicep"
            PesterTest                 = $true
            ValidationTest             = $false
            DeploymentTest             = $false
            ValidateOrDeployParameters = @{
                Location         = '<your location>'
                SubscriptionId   = '<your subscriptionId>'
                RemoveDeployment = $true
            }
            AdditionalTokens           = @{
                namePrefix = '<your prefix>'
                TenantId   = '<your tenantId>'
            }
        }
    
        # Run tests
    
        foreach ($testcase in $testcases) {
            Write-Output "Running test case $testcase on module $module"
            $TestModuleLocallyInput.ModuleTestFilePath = "$folder/avm/res/$module/tests/e2e/$testcase/main.test.bicep"
            Test-ModuleLocally @TestModuleLocallyInput
        }
    }
  7. Create a PR and reference the status badge of your pipeline run - see here.

    Note

    If you’re the sole owner of the module, the AVM core team must review and approve the PR. To indicate that your PR needs the core team’s attention, apply the Β Needs: Core Team 🧞  label on it!

  8. After a pull request has been created, it is important to update the AVM module proposal issue associated with your module, with a link to the pull request you created in BRM and mention the person who helped triage your module or the @Azure/avm-core-team-technical-bicep team.

  9. Once your BRM pull request has been approved and merged into main update the AVM module proposal issue associated with your module, with a Merged comment and mention the person who helped triage your module, or the @Azure/avm-core-team-technical-bicep team.

Generate Bicep Module Files

As per the module design structure (BCPNFR23), every module in the AVM library requires

  • a up-to-date ReadMe markdown (readme.md) file documenting the set of deployable resource types, input and output parameters and a set of relevant template references from the official Azure Resource Reference documentation
  • an up-to-date compiled template (main.json) file

The Set-AVMModule utility aims to simplify contributing to the AVM library, as it supports

  • idempotently generating the AVM folder structure for a module (including any child resource)
  • generating the module’s ReadMe file from scratch or updating it
  • compiling/building the module template

To ease maintenance, you can run the utility with a Recurse flag from the root of your folder to update all files automatically.

Location

You can find the script under utilities/tools/Set-AVMModule.ps1

How it works

Using the provided template path, the script

  1. validates the module’s folder structure
    • To do so, it searches for any required folder path / file missing and adds them. For several files, it will also provide some default content to get you started. The sources files for this action can be found here
  2. compiles its bicep template
  3. updates the readme (recursively, specified)
    1. If the intended readMe file does not yet exist in the expected path, it is generated with a skeleton (with e.g., a generated header name)
    2. The script then goes through all sections defined as SectionsToRefresh (by default all) and refreshes the sections’ content (for example, for the Parameters) based on the values in the ARM/JSON Template. It detects sections by their header and always regenerates the full section.
    3. Once all are refreshed, the current ReadMe file is overwritten. Note: The script can be invoked combining the WhatIf and Verbose switches to just receive an console-output of the updated content.

How to use it

For details on how to use the function, please refer to the script’s local documentation.

Note

The script must be loaded (’dot-sourced’) before the function can be invoked.

. 'C:/dev/Set-AVMModule.ps1'
Set-AVMModule (...)
Tip

For modules that require the generation of files on multiple-levels (for example, a module with child modules such as the ‘Key Vault’ module with its ‘Secret’ child module) it is highly recommended to make use of the -Recurse parameter.

This parameter will ensure that the script not only generates the files for the provided module folder path, but also all its nested module folder paths.

Tip

While readme files are always generated from scratch, you can add custom content is specific places that the script will preserve:

  • The module’s description in the main.bicep file’s metadata
  • The description of parameters & outputs
  • A section with the header ## Notes

If the utility finds a section with the heading ## Notes, it temporarily saves this content when it regenerates the readme file and then re-inserts (i.e. appends) the section towards the end of the readme file. This section may contain images, which must be stored in a subfolder /src in the root directory of the module.

Both for the text & images, please make sure to only add what provides tangible value as the content must be manually maintained and should not run stale. Further, for images, please make sure to only store them with an appropriate resolution & size to keep their impact on the repository’s size manageable.

Validate Module Locally

Use this script to test a module from your PC locally, without a CI environment. You can use it to run only the static validation (Pester tests), a deployment validation (dryRun) or an actual deployment to Azure. In the latter cases the script also takes care to replace placeholder tokens in the used module test & template files for you.

Location

You can find the script under utilities/tools/Test-ModuleLocally.ps1

How it works

If the switch for Pester tests (-PesterTest) is provided the script will

  1. Invoke the module test for the provided template file path and run all tests for it.

If the switch for either the validation test (-ValidationTest) or deployment test (-DeploymentTest) is provided alongside a HashTable for the token replacement (-ValidateOrDeployParameters), the script will

  1. Either fetch all module test files of the module’s tests folder (default) or you can specify a single module test file by leveraging the -ModuleTestFilePath parameter instead.
  2. Create a dictionary to replace all tokens in these module test files with actual values. This dictionary will consist
    • of the subscriptionID & managementGroupID of the provided ValidateOrDeployParameters object,
    • add all key-value pairs of the -AdditionalTokens object to it,
    • and optionally also add all key-value pairs specified in the settings.yml, under the ’local tokens settings'.
  3. If the -ValidationTest parameter was set, it runs a deployment validation using the Test-TemplateDeployment script.
  4. If the -DeploymentTest parameter was set, it runs a deployment using the New-TemplateDeployment script (with no retries).
  5. As a final step, it rolls the module test files back to their original state if either the -ValidationTest or -DeploymentTest parameters were provided.

How to use it

For details on how to use the function, please refer to the script’s local documentation.

Note

The script must be loaded (’dot-sourced’) before the function can be invoked.

. 'C:/dev/Test-ModuleLocally.ps1'
Test-ModuleLocally (...)
Important

Important: As the script emulates the testing logic of the CI environment, also tokens such as #_namePrefix_# are replaced by the script. However, in addition to the CI environment, it also reverses the token replacement to recover the files’ original state. As such, ensure that you use a namePrefix value that is unlikely to overlap with any string value in module folder you want to test.

For example, do not use avm, as the reverse token replacement would incorrectly replace the deployment name avmTelemetry found in each module to #_namePrefix_#Telemetry.

Bicep Contribution Prerequisites

You need to have a personal GitHub account which is linked to your Microsoft corporate identity. Once the link step is complete you must join the Azure organization.

Before you start contributing to the AVM, it is highly recommended that you complete the following Microsoft Learn paths, modules & courses:

Bicep

Git

Tooling

Required Tooling

To contribute to this project the following tooling is required:

The following tooling/extensions are recommended to assist you developing for the project:

Visual Studio Code Extensions

Desktop Tooling

  • GitHub Desktop
    • To enhance streamlined integration during interactions with upstream repositories, GitHub Desktop will automatically configure your local git repository to use the upstream repository as a remote.

Contribution Q&A

Tip

Check out the FAQ for more answers to common questions about the AVM initiative in general.

Proposing a module


Who can propose a new module and where can I submit a new module proposal / request?

Everyone can propose a module

To propose a new module, simply create an issue/complete the form here.


Can I just propose / create any module?

For example, can I propose one for managed disks or NICs or diagnostic settings? What about patterns?

No, you cannot propose or create just any module. You can only propose modules that are aligned with requirements documented in the module specifications section.

Below, we provide some guidance on what modules you can / cannot propose.

  • Resource modules: resource modules have bring extra value to the end user (can’t just be simple wrappers) and MUST mapped 1:1 to RPs (resource providers) and top level resources. You MUST follow the module specifications and your modules SHOULD be WAF aligned.

    • Good examples:
      • Virtual machine: the VM module is highly complex and therefore, it brings extra value to the end user by providing a wide variety of features (e.g., diagnostics, RBAC, domain join, disk encryption, backup and more).
      • Storage account: even though, this module is mainly built around one RP, it brings extra value by providing easy access to its child resources, such as file/table/queue services, as well as additional standard interfaces (e.g., diagnostics, RBAC, encryption, firewall, etc.).
    • Bad examples:
      • NIC or Public IP (PIP) module: these would be simple wrappers around the NIC/PIP resource and wouldn’t bring any extra value. NICs and PIPs SHOULD be surfaced as part of the VM module (or any other primary resources that require them).
      • Diagnostic settings: these are too low-level “sub resources”, and highly dependent on their “primary resource’s” RP defined as “interfaces” and therefore MUST be used as part of a resource module holding a primary resource - see Diagnostic Settings documentation about the correct implementation.
  • Pattern modules: In case of pattern modules, ideally you should start from architectural patterns, published in the Azure Architecture Center, and build your pattern module by leveraging resource modules that are required to implement the pattern. AVM does not provide architectural guidance on how you should design your pattern, but you MUST follow the module specifications and your modules SHOULD be WAF aligned.

    • Good examples:
      • Landing zone accelerators for N-tier web application; AKS cluster; SAP: there are numerous examples for these architectures in Azure Architecture Center that already have baked in guidance / smart defaults that are WAF Aligned, therefore these are good candidates for pattern modules. Module owners MAY leverage resource modules to implement the pattern.
      • Hub and spoke topology: it’s a common pattern that is used by many customers and there are great examples available through Azure Architecture Center, as well as Azure Landing Zones. Also a good candidate for a pattern module.
    • Bad examples:
      • A pair of Virtual machines: being a simple wrapper, this solution wouldn’t bring any extra value as it doesn’t provide a complete solution.
      • Key Vault that deploys automatically generated secrets: this is aligned with the definition of a resource modules, therefore it should be categorized as such.

Where do I need to go to make sure the module I’d like to propose is not already in the works?

The AVM core team maintains the list of Bicep and Terraform modules and tracks the status of each module. Based on this list, you can check if the module you’d like to build is already in the works (e.g., it’s being worked on in a feature branch but hasn’t been published yet).

To see the formatted lists with additional information, please visit the AVM Module Indexes page.


I need a new module but I cannot own/author it for various reasons, what should I do?

Each AVM module requires a module owner and MAY have additional module contributors.

Essentially, you have 3 options:

  1. You sign up to be a module owner (and optionally, you can find additional contributors to help you).
  2. You find / request someone else to be the module owner (and optionally, you can be a contributor).
  3. You propose a module and wait until the AVM core team finds a module owner for you (who then can optionally leverage the help of additional contributors).

As these options are increasingly more time consuming, we recommend you to start with considering option 1 and only if you cannot own the module, should you move to option 2 and then 3.

You can propose a new module here.


How long will it take for someone to respond and a module to be created/updated and published?

While there are SLAs defined for providing support for existing modules, there are currently no SLAs in place for the creation of new modules. The AVM core team is a small team and is currently working on automating the module creation process to make it as easy as possible for module owners to create and publish modules on their own.

Beside of providing program level governance, the AVM core team is mainly responsible for defining the module specifications, providing tooling (such as test frameworks and pipelines), guidance and support to module owners, as well as facilitating the creation of new modules by maintaining the module catalog and identifying volunteers for owning the modules. However, modules will be created and maintained by a broader community of module owners.


How do I let the AVM team know I really need an AVM module to unblock me / my project / my company?

  • If you’re an external user, you can propose a module here and provide as much context as possible under the “Module Details” section (e.g., why do you need the module, what’s the business impact of not having it, etc.).

  • If you’re a Microsoft employee and have already proposed a module here, you can reach out to the AVM core team directly via Teams to provide more details internally.

The AVM core team will then triage the request and get back to you with next steps. You can accelerate the process of creating the module by volunteering to be a module owner.

Developing a module


Who is developing a modules?

Every module has an owner that is responsible for module development and maintenance. One owner can own one or multiple modules. An owner can develop modules alone or lead a team that will develop a module.
If you want to join a team and to contribute on specific module, please contact module owner.

At this moment, only Microsoft FTEs can be module owners.


What do I need so I can start developing a module?

We suggest that you review module specification and contribution guides:

Feel free to reach out to the AVM Core team in case that additional help is needed.


What do I do about existing modules that are available doing a similar thing to my module that I am proposing to develop and release?

As part of the Module Proposal process, the AVM core team will work with you to triage your proposal. We also want to make sure that no similar existing modules from known Microsoft projects are already on their way to be migrated to AVM.

  • If there aren’t any, then you can proceed with developing your module from scratch once given approval to proceed by the AVM core team.
  • However, if there are existing modules from Microsoft projects we would invite you to help us complete the migration to AVM of this module; this may also entail working with the existing module owner/team.

For existing modules that may not be directly owned and developed by Microsoft or their employees you should first review the license applied to the GitHub repository hosting the module and understand its terms and conditions. More information on GitHub repositories and licenses can be found here in Licensing a repository Most modules will use a license that will allow you to take inspiration and copy all or parts from the module source code. However, to confirm, you should always check the license and any conditions you may have to meet by doing this.


What are the mandatory labels that needs to be used while managing issues, pull requests and discussions on GitHub repositories where module are held?

To get list of labels that MUST be created on gitHub repositories where modules are held navigate to Shared non-functional requirement 23 (SNFR23).

You SHOULD NOT use any additional labels.

There is also a PowerShell script that the AVM core team created that can help to apply those labels the GitHub module repository.


Is there any naming convention for modules name, repository name, variables, parameters…. ?

AVM specification covers all naming conventions. As example:
Module naming specification


Where module will live? Do I need to create separate repo or to place it in specific folder?

Bicep

For Bicep, both Resource and Pattern, AVM Modules will be homed in the Azure/bicep-registry-modules repository and live within an avm directory that will be located at the root of the repository.

If you are module owner, it is expected that you will fork the Azure/bicep-registry-modules repository and work on a branch from within their fork, before then creating a Pull Request (PR) back into the Azure/bicep-registry-modules repositories main branch. In Bice contribution guide, you can discover Directory and File structure that will be used and examples.

Terraform

Each Terraform AVM module will have its own GitHub Repository in the Azure GitHub Organization.
This repo will be created by the Module Owners and the AVM Core team collaboratively, including the configuration of permissions.
To read more about how to start, navigate to Terraform AVM contribution guide.


I get the error ‘The repository ********** already exists on this account’ when I try to create a new repository, what should I do?

If you get this error, it means that the repository already exists in the Azure GitHub Organization. This can happen if someone has already created a repository with the same name in the past and then archived it.

To determine if this is the case you’ll need to navigate to the Microsoft Open Source Management Portal, then search for the repository name you are trying to create. Click on the repository and you will find the owner. Reach out the owner to ask them to transfer the repo to you or delete it. You’ll want them to delete it if it was not created from the template.


Where can I test my module during development?

During initial module development module owners/developers need to use your own environment (Azure subscriptions) to test module. In later phase, during publishing process, we will conduct automated test that will use AVM dedicated environment.

Updating and managing a module


I’m already using a module today, but its missing a feature, what should I do?

You should use GitHub issues to propose changes or improvements for specific module. Issue request will be routed to module owner that MUST respond to logged issues as per the defined support statement. In case that module currently don’t have owner, AVM Core Team will handle request.


I am using module without owner. What will happened if I need update?

AVM core team will work to assign owner for every module, but it can happen during a time that there are modules without owner. If you would like to own that module, feel free to ask to take ownership. At this moment, only Microsoft FTEs can be module owners.


How will the support SLAs be automatically enforced?

All issues created in a module repo will be automatically be picked up and tracked by the GitHub Policy Service. This service will take the necessary steps when escalation is needed as per the SLAs defined in the Module Support chapter.

Process Overview

This page provides an overview of the contribution process for AVM modules.

New Module Proposal & Creation

Important

Each AVM module MUST have a Module Proposal issue created and approved by the AVM core team before it can be created/migrated!

---
config:
  nodeSpacing: 20
  rankSpacing: 20
  diagramPadding: 5
  padding: 5
  useWidth: 100
  flowchart:
    wrappingWidth: 400
    padding: 5
---
flowchart TD
    ModuleIdea[Consumer has an idea for a new AVM Module] -->CheckIndex(Check AVM Module Indexes)
        click CheckIndex "/Azure-Verified-Modules/indexes/"
    CheckIndex -->IndexExistenceCheck{Is the module<br>in the index?}
    IndexExistenceCheck -->|No|A
    IndexExistenceCheck -->|Yes|EndExistenceCheck(Review existing/proposed AVM module)
    EndExistenceCheck -->OrphanedCheck{ Is the module<br>orphaned? }
        click OrphanedCheck "/Azure-Verified-Modules/specs/shared/module-lifecycle/#orphaned-avm-modules"
    OrphanedCheck -->|No|ContactOwner[Contact module owner,<br> via GitHub issues on the related <br>repo, to discuss enhancements/<br>bugs/opportunities to contribute etc.]
    OrphanedCheck -->|Yes|OrphanOwnerYes(Locate the related issue <br> and comment on:<br> - A feature/enhancement suggestion <br> - Indicating you wish to become the owner)
        click OrphanOwnerYes "/Azure-Verified-Modules/specs/shared/module-lifecycle/#orphaned-avm-modules"
    OrphanOwnerYes -->B
    A[[ Create Module Proposal ]] -->|GitHub Issue/Form Submitted| B{ AVM Core Team<br>Triage }
        click A "https://aka.ms/avm/moduleproposal"
        click B "/Azure-Verified-Modules/help-support/issue-triage/avm-issue-triage/#avm-core-team-triage-explained"
    B -->|Module Approved for Creation| C[["Module Owner(s) Identified  & assigned to GitHub issue/proposal" ]]
    B -->|Module Rejected| D(Issue closed with reasoning)
    C -->E[[ Module index CSV files updated by AVM Core Team]]
        click E "/Azure-Verified-Modules/indexes/"
    E -->E1[[Repo/Directory Created following the <br> Contribution Guide ]]
        click E1 "/Azure-Verified-Modules/contributing/"
    E1 -->F("Module Developed by Owner(s) & their Contributors")
    F -->G[[ Module & AVM Compliance Tests ]]
        click G "/Azure-Verified-Modules/spec/SNFR3"
    G -->|Tests Fail|I(Modules/Tests Fixed <br> To Make Them Pass)
    I -->F
    G -->|Tests Pass|J[[Version 0.1.0 created]]
    J -->K[[Publish to Bicep/Terraform Registry]]
    K -->L(Take Feedback from v0.1.0 Consumers)
    L -->M{Anything<br>to be resolved <br> before 1.0.0<br>release? }
        click M "/Azure-Verified-Modules/contributing/process/#avm-preview-notice"
    M -->|Yes|FixPreV1("Module feedback incorporated by Owner(s) & their Contributors")
    FixPreV1 -->PreV1Tests[[Self & AVM Module Tests]]
    PreV1Tests -->|Tests Fail|PreV1TestsFix(Modules/Tests Fixed To Make Them Pass)
    PreV1TestsFix -->N
    M -->|No|N[[Publish 1.0.0 Release]]
    N -->O[[Publish to IaC Registry]]
    O -->P[[ Module BAU Starts ]]
        click P "/Azure-Verified-Modules/help-support/module-support/"

Provide details for module proposals

When proposing a module, please include the information in the description that is mentioned for the triage process here:

AVM Preview Notice

Important

The AVM framework continues to evolve, and several elements, such as Continuous Integration (CI) processes, module specifications and corresponding specification‑validation coverage, are not yet fully implemented. Hence, modules MUST NOT be published at version 1.0.0 or higher at this time.

All module MUST be published as a 0.x.y minor version (e.g., 0.1.0, 0.1.1, 0.2.0, etc.) until the AVM team provides guidance that publishing v1.0.0 is allowed.

However, it is important to note that this DOES NOT mean that the modules cannot be consumed and utilized. They CAN be leveraged in all types of environments (dev, test, prod etc.). Consumers can treat them just like any other IaC module and raise issues or feature requests against them as they learn from the usage of the module. Consumers should also read the release notes for each version, if considering updating to a more recent version of a module to see if there are any considerations or breaking changes etc.

Module Owner Has Issue/Is Blocked/Has A Request

In the event that a module owner has an issue or is blocked due to specific AVM missing guidance, test environments, permission requirements, etc. they should follow the below steps:

Tip

Common issues/blockers/asks/request are:

  • Subscription level features
  • Resource Provider Registration
  • Preview Services Enablement
  • Entra ID (formerly Azure Active Directory) configuration (SPN creation, etc.)
  1. Create a GitHub Issue
  2. Discuss the issue/blocker with the AVM core team
  3. Agree upon action/resolution/closure
  4. Implement agreed upon action/resolution/closure
Note

Please note for module specific issues, these should be logged in the module’s source repository, not the AVM repository.

Terraform Contribution Guide

Important

While this page describes and summarizes important aspects of contributing to AVM, it only references some of the shared and language specific requirements.

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

Summary

This section lists AVM’s Terraform-specific contribution guidance.

Subsections of Terraform Modules

Prerequisites

GitHub Account

To contribute, you need a GitHub account. If you are a Microsoft employee, your account must be linked to your corporate identity and you must be a member of the Azure organization.

Required Tooling

Tip

We recommend Linux, macOS, or Windows Subsystem for Linux (WSL) for your development environment.

Terraform Contribution Flow

This guide covers the end-to-end contribution flow for AVM Terraform modules.
Whether you are a module owner or an external contributor, the core workflow is the same β€” the key differences are called out using tabs below.

Important

This guide MUST be used in conjunction with the Terraform specifications. All AVM modules must meet the requirements described in those specifications.

Overview


---
config:
  nodeSpacing: 20
  rankSpacing: 20
  diagramPadding: 50
  padding: 5
  flowchart:
    wrappingWidth: 300
    padding: 5
  layout: elk
  elk:
    mergeEdges: true
    nodePlacementStrategy: LINEAR_SEGMENTS
---

flowchart TD
  Z("1 - Fork [optional]")
    click Z "#1-fork-optional"
  A(2 - Branch)
    click A "#2-branch"
  B(3 - Implement your code change)
    click B "#3-implement-your-code-change"
  C(4 - Run avm pre-commit)
    click C "#4-run-avm-pre-commit"
  C2(5 - Run pr-check and e2e tests locally)
    click C2 "#5-run-pr-check-and-e2e-tests-locally"
  D(6 - Raise or Update PR)
    click D "#6-raise-or-update-pr"
  E("7 - Approve and monitor CI tests [owner]")
    click E "#7-approve-and-monitor-ci-tests"
  F{Tests passing?}
  G(8 - Review and merge PR)
    click G "#8-review-and-merge-pr"
  H(9 - Cut a release)
    click H "#9-cut-a-release"
  Z --> A
  A --> B
  B --> C
  C --> C2
  C2 --> D
  D --> E
  E --> F
  F -->|no| B
  F -->|yes| G
  G --> H


1. Fork [optional]

Note

This step is only needed if you do not have write access to the module repository. Module owners and invited collaborators can skip to step 2.

A fork is your own copy of the repository under your GitHub account. It lets you make changes without needing write access to the upstream repo. Once your changes are ready, you raise a pull request from your fork back to the original repository.

  1. Navigate to the module repository in the Azure GitHub organization.

  2. Click the Fork button in the top right.

  3. Select your GitHub account (or organization) as the destination.

  4. Click Create fork.

  5. Clone your fork locally:

    git clone https://github.com/<your-username>/terraform-azurerm-avm-res-<rp>-<modulename>.git
    cd terraform-azurerm-avm-res-<rp>-<modulename>

Keep your fork in sync with the upstream repository before creating a new branch. You can do this from the GitHub UI by clicking Sync fork on your fork’s main page, or locally:

git remote add upstream https://github.com/Azure/terraform-azurerm-avm-res-<rp>-<modulename>.git
git fetch upstream
git checkout main
git merge upstream/main

Use the GitHub CLI to fork and clone in one step. This automatically configures the upstream remote for you:

gh repo fork Azure/terraform-azurerm-avm-res-<rp>-<modulename> --clone
cd terraform-azurerm-avm-res-<rp>-<modulename>

Verify the remotes are set up correctly:

git remote -v
# origin    https://github.com/<your-username>/terraform-azurerm-avm-res-<rp>-<modulename>.git (fetch)
# upstream  https://github.com/Azure/terraform-azurerm-avm-res-<rp>-<modulename>.git (fetch)

2. Branch

Create a branch from main to work on your changes:

git checkout -b <your-branch-name>

If this is a new module and the repository does not exist yet, module owners should first follow the Repository Creation Process.

Note

If the module repository does not exist yet, check the Terraform Resource Modules index for the module owner’s contact details (PrimaryModuleOwnerGHHandle column).


3. Implement your code change

Before writing code, review the Terraform specifications and composition guidelines to ensure your contribution complies with AVM’s design principles.

Once you’ve made your changes, stage, commit, and push them:

git add -A
git commit -m "feat: description of your change"
git push

4. Run avm pre-commit

Before raising a pull request, run pre-commit to update your files:

Important

You need Docker (or Podman) installed and running.

./avm pre-commit

This automatically updates your code formatting, fixes styling issues, and regenerates documentation to meet AVM standards. If pre-commit made any changes, commit and push again:

git add -A
git commit -m "chore: pre-commit fixes"
git push

5. Run pr-check and e2e tests locally

Before raising a PR (or while iterating on one), you can run the same checks that CI will run:

./avm pr-check

This runs static analysis and linting locally so you can catch issues before CI does.

Local e2e testing

You can test your examples locally by running Terraform directly in the examples/ folders:

cd examples/default
az login
terraform init
terraform plan
terraform apply

Use Azure CLI (az login) to authenticate β€” no environment variables or service principals are needed for local development.

When you’re done, clean up your resources:

terraform destroy

This is especially useful for external contributors, since only module owners can approve CI e2e test runs.

Terraform test (optional)

We support terraform test for unit and integration testing. Golang tests are not supported.

  • Unit tests β€” place test files in tests/unit. Use mocked providers to keep them fast and free of external dependencies. Run with ./avm tf-test-unit.
  • Integration tests β€” place test files in tests/integration. These deploy real resources and should be run locally. Run with ./avm tf-test-integration.

6. Raise or Update PR

Tip

Raise your PR early β€” don’t wait until everything is perfect. An early PR lets you run pr-check and e2e tests in CI and get feedback sooner. You can continue pushing commits to the same branch.

  1. Navigate to the upstream repository on GitHub and click New pull request.
  2. Set the base repository to the upstream AVM repo and base branch to main.
  3. Set your head repository and compare branch to your fork and branch.
  4. Click Create pull request.
  1. Navigate to the repository on GitHub and click New pull request.
  2. Set the base branch to main and the compare branch to your branch.
  3. Click Create pull request.

7. Approve and monitor CI tests

Note

Only module owners can approve CI test runs. External contributors should ensure they have run ./avm pr-check and tested locally before this step.

Once a PR is created, CI workflows are triggered automatically but require a module owner to approve the run. A centrally managed Azure test subscription is provided β€” no credential configuration is needed.

What CI runs

The pr-check.yml workflow runs two stages:

Linting β€” static analysis including:

  • avmfix β€” formatting checks.
  • terraform-docs β€” documentation is up to date.
  • TFLint β€” AVM spec compliance.
  • Conftest β€” checks the plan for Well-Architected Framework compliance using Conftest and OPA.

End-to-end tests β€” deploys and validates all module examples:

  1. Lists all examples in the examples/ directory.
  2. Tests each example for idempotency (terraform apply then terraform plan).
  3. Destroys all resources (terraform destroy).

If tests fail

Go back to step 3 β€” fix the issue, run ./avm pre-commit again, push your changes, and the CI tests will re-run automatically on the same PR.

Running e2e for external contributions

When approving a PR from an external contributor:

  1. Review the code for security β€” check for any malicious code or changes to workflow files before running tests. If found, close the PR and report the contributor.
  2. Create a release branch from main (e.g. release/<description>).
  3. Change the PR’s base branch to the release branch and merge it.
  4. Create a new PR from the release branch to main β€” this triggers pr-check and e2e tests.
  5. Approve the run and wait for results.
  6. If tests fail, send back to the contributor to fix and repeat from step 3.

Running e2e for your own contributions

For your own PRs, the tests trigger automatically β€” approve the run and wait for results.


8. Review and merge PR

Every PR must be reviewed and approved before merging.

PR is submitted by a module ownerPR is submitted by anyone, other than the module owner
Module has a single module ownerAVM core team or in case of Terraform only, the owner of another module approves the PRModule owner approves the PR
Module has multiple module ownersAnother owner of the module (other than the submitter) approves the PROne of the owners of the module approves the PR
  • Address any review comments and push updates to your branch.
  • Request a re-review once changes are made.
  • The module owner will merge the PR once approved and tests pass.

For a brand new module being published for the first time, get the module reviewed by the AVM Core team by following the AVM Review Process before merging.

Owner responsibilities


9. Cut a release

Note

This step is performed by the module owner only.

After the PR is merged to main, create a release via GitHub Releases:

  1. Go to the Releases tab and click Draft a new release.
  2. Set Target to the main branch.
  3. Type a new tag (e.g. v0.1.0 for first publish, or increment for subsequent releases). Tags MUST include the v prefix.
  4. Use Generate release notes and credit external contributors.
  5. Click Publish release.

First module publish

For a brand new module, contact the AVM core team (e.g. via the AVM - Module Triage project) to request initial publication to the HashiCorp Registry. Subsequent releases are published automatically.

Important

Continue publishing in the v0.x.y range (e.g., v0.1.0, v0.1.1, v0.2.0) until the AVM team notifies you that v1.0.0 is allowed.


Common mistakes to avoid

  • Search and update TODO comments that come from the template β€” remove them once addressed.
  • Do not commit terraform.lock.hcl β€” it is excluded by .gitignore.
  • Update _header.md and SUPPORT.md.
  • Do not commit terraform.tfvars files.

Terraform Composition

Important

This guide MUST be used in conjunction with the Terraform specifications. ALL AVM modules (Resource and Pattern modules) MUST meet the respective requirements described in these 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 template repo here.

  • tests/ - (for unit tests and integration tests using Terraform test)
    • unit/ - (optional .tftest.hcl files for unit testing with Terraform test)
    • integration/ - (optional .tftest.hcl files for integration testing with Terraform test)
  • modules/ - (for sub-modules only if used)
  • examples/ - (all examples must deploy successfully without requiring input - these are customer facing)
    • <at least one folder> - (at least one example that uses the variable defaults minimum/required parameters/variables only)
    • <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
    • README.md (autogenerated)
    • main.resource1.tf (If a larger module you may chose to use dot notation for each resource)
    • locals.resource1.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/Outputs within the Terraform specification pages.

Tip

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.

Tip

See examples in specifications SFR1 and RMFR1.

Outputs

Make sure to review all specifications of Category: Inputs/Outputs within the Terraform specification pages.

Tip

See examples in specification RMFR7 and TFFR2.

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.

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 is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo. 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"
    }
  }
}

Review of Terraform Modules

The AVM module review is a critical step before an AVM Terraform module gets published to the Terraform Registry and made publicly available for customers, partners and wider community to consume and contribute to. It serves as a quality assurance step to ensure that the AVM Terraform module complies with the Terraform specifications of AVM. The below process outlines the steps that both the module owner and module reviewer need to follow.

  1. The module owner completes the development of the module in their branch or fork.

  2. The module owner submits a pull request (PR) titled AVM-Review-PR and ensures that all checks are passing on that PR as that is a pre-requisite to request a review.

  3. The module owner assigns the avm-core-team-technical-terraform GitHub team as reviewer on the PR.

  4. The module owner leaves the following comment as it is on the module proposal in the AVM - Module Triage project by searching for their module proposal by name there.

    βž• AVM Terraform Module Review Request
    I have completed my initial development of the module and I would like to request a review of my module before publishing it to the Terraform Registry. The latest code is in a PR titled [AVM-Review-PR](REPLACE WITH URL TO YOUR PR) on the module repo and all checks on that PR are passing.
  5. The AVM team moves the module proposal from “In Development” to “In Review” in the AVM - Module Triage project.

  6. The AVM team will assign a module reviewer who will open a blank issue on the module titled “AVM-Review” and populate it with the below mark down. This template already marks the specs as compliant which are covered by the checks that run on the PR. There are some specs which don’t need to be checked at the time of publishing the module therefore they are marked as NA.

    βž• AVM Terraform Module Review Issue

    Dear module owner,

    As per the module ownership requirements and responsibilities at the time of [assignment](REPLACE WITH THE LINK TO THE AVM MODULE PROPOSAL), the AVM Team is opening this issue, requesting you to validate your module against the below AVM specifications and confirm its compliance.

    Please don’t close this issue and merge your AVM-Review-PR until advised to do so. This review is a prerequisite for publishing your module’s v0.1.0 in the Terraform Registry. The AVM team is happy to assist with any questions you might have.

    Requested Actions

    1. Complete the below task list by ticking off the tasks.
    2. Complete the below table by updating the Compliant column with Yes, No or NA as possible values.

    Please use the comments columns to provide additional details especially if the Compliant column is updated to No or NA.

    Tasks

    • Address comments on AVM-Review-PR if any
    • Ensure that all checks on AVM-Review-PR are passing
    • Make sure you have run pre-commit.
    • Tick this to acknowledge specs with comment “Module Owner to action this spec post-publish as appropriate” in the table below.
    • Please update the _header.md file as it contains instructions which - once actioned - need to be replaced with Module Name and Description.
    IDSpecCompliantComments
    1ID: SFR1 - Category: Composition - Preview ServicesNA if no preview services are used
    2ID: SFR2 - Category: Composition - WAF AlignedEnsure only high priority reliability & security recommendations are implemented if any
    3ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry
    4ID: SFR4 - Category: Telemetry - Telemetry Enablement FlexibilityYesYes if AVM Template Repo has been used
    5ID: SFR5 - Category: Composition - Availability Zones
    6ID: SFR6 - Category: Composition - Data Redundancy
    7ID: SNFR25 - Category: Composition - Resource Naming
    8ID: SNFR1 - Category: Testing - Prescribed TestsYesYes if all e2e test, version-check & linting checks passed
    9ID: SNFR2 - Category: Testing - E2E TestingYesYes if e2e tests passed
    10ID: SNFR3 - Category: Testing - AVM Compliance TestsYesYes if all e2e test, version-check & linting checks passed
    11ID: SNFR4 - Category: Testing - Unit TestsNA if no tests created in tests folder
    12ID: SNFR5 - Category: Testing - Upgrade TestsNAModule Owner to action this spec post-publish as appropriate
    13ID: SNFR6 - Category: Testing - Static Analysis/Linting TestsYesYes if all linting checks passed
    14ID: SNFR7 - Category: Testing - Idempotency TestsYesYes if e2e tests passed
    15ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface ResourcesYesYes if e2e tests passed
    16ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub
    17ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only
    18ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions
    19ID: SNFR10 - Category: Contribution/Support - MIT LicensingYesYes if AVM Template Repo has been used
    20ID: SNFR11 - Category: Contribution/Support - Issues Response TimesNAModule Owner to action this spec post-publish as appropriate
    21ID: SNFR12 - Category: Contribution/Support - Versions SupportedNAModule Owner to action this spec post-publish as appropriate
    22ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels
    23ID: SNFR14 - Category: Inputs - Data Types
    24ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs
    25ID: SNFR15 - Category: Documentation - Automatic Documentation GenerationYesYes if linting / docs check passed
    26ID: SNFR16 - Category: Documentation - Examples/E2EYesYes if e2e tests passed
    27ID: SNFR17 - Category: Release - Semantic VersioningYesYes if version-check check passed
    28ID: SNFR18 - Category: Release - Breaking ChangesNAModule Owner to action this spec post-publish as appropriate
    29ID: SNFR19 - Category: Publishing - Registries TargetedNAModule Owner to action this spec post-publish as appropriate
    30ID: SNFR21 - Category: Publishing - Cross Language CollaborationNAModule Owner to action this spec post-publish as appropriate
    31ID: RMFR1 - Category: Composition - Single Resource Only
    32ID: RMFR2 - Category: Composition - No Resource Wrapper Modules
    33ID: RMFR3 - Category: Composition - Resource Groups
    34ID: RMFR4 - Category: Composition - AVM Consistent Feature & Extension Resources Value AddYesYes if linting / terraform check passed
    35ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/SchemasYesYes if linting / terraform check passed
    36ID: RMFR8 - Category: Composition - Dependency on child and other resources
    37ID: RMFR6 - Category: Inputs - Parameter/Variable Naming
    38ID: RMFR7 - Category: Outputs - Minimum Required OutputsYesYes if linting / terraform check passed
    39ID: RMNFR1 - Category: Naming - Module Naming
    40ID: RMNFR2 - Category: Inputs - Parameter/Variable Naming
    41ID: RMNFR3 - Category: Composition - RP CollaborationNAModule Owner to action this spec post-publish as appropriate
    42ID: PMFR1 - Category: Composition - Resource Group CreationNA if this is not a pattern module
    43ID: PMNFR1 - Category: Naming - Module NamingNA if this is not a pattern module
    44ID: PMNFR2 - Category: Composition - Use Resource Modules to Build a Pattern ModuleNA if this is not a pattern module
    45ID: PMNFR3 - Category: Composition - Use other Pattern Modules to Build a Pattern ModuleNA if this is not a pattern module
    46ID: PMNFR4 - Category: Hygiene - Missing Resource Module(s)NA if this is not a pattern module
    47ID: PMNFR5 - Category: Inputs - Parameter/Variable NamingNA if this is not a pattern module
    48ID: TFFR1 - Category: Composition - Cross-Referencing Modules
    49ID: TFFR2 - Category: Outputs - Additional Terraform OutputsYesYes if linting / terraform check passed
    50ID: TFNFR1 - Category: Documentation - Descriptions
    51ID: TFNFR2 - Category: Documentation - Module Documentation GenerationYesYes if linting / docs check passed
    52ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch ProtectionYesYes if AVM Template Repo has been used
    53ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casingYesYes if linting / terraform check passed
    54ID: TFNFR5 - Category: Testing - Test ToolingYesYes if linting / terraform check passed
    55ID: TFNFR6 - Category: Code Style - Resource & Data Order
    56ID: TFNFR7 - Category: Code Style - count & for_each Use
    57ID: TFNFR8 - Category: Code Style - Resource & Data Block OrdersYesYes if linting / avmfix check passed
    58ID: TFNFR9 - Category: Code Style - Module Block Order
    59ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes
    60ID: TFNFR11 - Category: Code Style - Null Comparison Toggle
    61ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects
    62ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try
    63ID: TFNFR14 - Category: Inputs - Not allowed variables
    64ID: TFNFR15 - Category: Code Style - Variable Definition OrderYesYes if linting / avmfix check passed
    65ID: TFNFR16 - Category: Code Style - Variable Naming RulesYesYes if linting / terraform check passed
    66ID: TFNFR17 - Category: Code Style - Variables with DescriptionsYesYes if linting / terraform check passed
    67ID: TFNFR18 - Category: Code Style - Variables with TypesYesYes if linting / terraform check passed
    68ID: TFNFR19 - Category: Code Style - Sensitive Data Variables
    69ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values
    70ID: TFNFR21 - Category: Code Style - Discourage Nullability by DefaultYesYes if linting / avmfix check passed
    71ID: TFNFR22 - Category: Code Style - Avoid sensitive = falseYesYes if linting / avmfix check passed
    72ID: TFNFR23 - Category: Code Style - Sensitive Default Value ConditionsYesYes if linting / terraform check passed
    73ID: TFNFR24 - Category: Code Style - Handling Deprecated VariablesNAModule Owner to action this spec post-publish as appropriate
    74ID: TFNFR25 - Category: Code Style - Verified Modules RequirementsYesYes if linting / terraform check passed
    75ID: TFNFR26 - Category: Code Style - Providers in required_providersYesYes if linting / terraform check passed
    76ID: TFNFR27 - Category: Code Style - Provider Declarations in ModulesYesYes if linting / terraform check passed
    77ID: TFNFR29 - Category: Code Style - Sensitive Data OutputsYesYes if linting / avmfix check passed
    78ID: TFNFR30 - Category: Code Style - Handling Deprecated OutputsNAModule Owner to action this spec post-publish as appropriate
    79ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only
    80ID: TFNFR32 - Category: Code Style - Alphabetical Local ArrangementYesYes if linting / avmfix check passed
    81ID: TFNFR33 - Category: Code Style - Precise Local Types
    82ID: TFNFR34 - Category: Code Style - Using Feature TogglesNAModule Owner to action this spec post-publish as appropriate
    83ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking ChangesNAModule Owner to action this spec post-publish as appropriate
    84ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources
    85ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner
  7. The module reviewer can update the Compliance column for specs in line 42 to 47 to NA, in case the module being reviewed isn’t a pattern module.

  8. The module reviewer reviews the code in the PR and leaves comments to request any necessary updates.

  9. The module reviewer assigns the AVM-Review issue to the module owner and links the AVM-Review Issue to the AVM-Review-PR so that once the module reviewer approves the PR and the module owner merges the AVM-Review-PR, the AMV-Review issue is automatically closed. The module reviews responds to the module owner’s comment on the Module Proposal in AVM Repo with the following

    βž• AVM Terraform Module Review Initiation Message
    Thank you for requesting a review of your module. The AVM module review process has been initiated, please perform the **Requested Actions** on the AVM-Review issue on the module repo.
  10. The module owner updates the check list and the table in the AVM-Review issue and notifies the module reviewer in a comment.

  11. The module reviewer performs the final review and ensures that all checks in the checklist are complete and the specifications table has been updated with no requirements having compliance as ‘No’.

  12. The module reviewer approves the AVM-Review-PR, and leaves the following comment on the AVM-Review issue with the following comment.

    βž• AVM Terraform Module Review Completion Message
    Thank you for contributing this module and completing the review process per AVM specs. The AVM-Review-PR has been approved and once you merge it that will close this AVM-Review issue. Please create a release with an initial minor version of `v0.1.0` (tags **MUST** include the `v` prefix) and then contact the AVM core team to publish this module to the HashiCorp Terraform Registry via HCP Terraform. Please continue publishing future versions in the v0.x.y minor range (e.g., `v0.1.0`, `v0.1.1`, `v0.2.0`, etc.) until the AVM team notifies you that publishing `v1.0.0` is allowed.
    
    **Requested Action**: Once the AVM core team has published the module, please update your [module proposal](REPLACE WITH THE LINK TO THE MODULE PROPOSAL) with the following comment.
    
    "The initial review of this module is complete, and the module has been published to the registry by the AVM core team. Requesting AVM team to close this module proposal and mark the module available in the module index.
    Terraform Registry Link: <REPLACE WITH THE LINK OF THE MODULE IN TERRAFORM REGISTRY>
    GitHub Repo Link: <REPLACE WITH THE LINK OF THE MODULE IN GITHUB>"
  13. Once the module owner perform the requested action in the previous step, the module reviewer updates the module proposal by performing the following steps:

  • Assign label Status: Module Available :green_circle: to the module proposal.
  • Update the module index excel file and CSV file by creating a PR to update the module index and links the module proposal as an issue that gets closed once the PR is merged which will move the module proposal from “In Review” to “Done” in the AVM - Module Triage project.

Advanced Topics & FAQ

This page covers advanced scenarios and frequently asked questions that go beyond the standard contribution flow.


Using a custom Azure test subscription

By default, CI end-to-end tests run against a centrally managed Azure subscription. If your module requires a different environment (e.g. due to quota limits or tenant-level deployments), you can override the defaults.

  1. Create a user-assigned managed identity in your target Azure environment.
  2. Create GitHub federated credentials for the managed identity, using the module’s GitHub organization and repository. Select entity type environment and set the name to test.
  3. Assign appropriate roles to the managed identity.
  4. Elevate your access via the Open Source Portal.
  5. Go to the repository Settings > Environments > test and add the following secrets:
    • ARM_CLIENT_ID_OVERRIDE β€” Client ID of the managed identity.
    • ARM_TENANT_ID_OVERRIDE β€” Tenant ID.
    • ARM_SUBSCRIPTION_ID_OVERRIDE β€” Subscription ID.

Custom variables and secrets for end-to-end tests

The test environment in each module repository has approvals and secrets configured for e2e tests. If you need additional variables or secrets:

  • Add them to the test environment in your repository settings.
  • They must be prefixed with TF_VAR_ β€” other prefixes will be ignored.

OPA (Conftest) policy exceptions

Conftest checks the plan for compliance with the Well-Architected Framework using OPA policies.

If you get policy failures that need an exception, create a .rego file in the exceptions sub-directory of the relevant example:

package Azure_Proactive_Resiliency_Library_v2
import rego.v1
exception contains rules if {
  rules = ["configure_aks_default_node_pool_zones"]
}

TFLint rule overrides

TFLint checks AVM spec compliance using the AVM custom ruleset.

To override a rule, create one of the following HCL files in the root of your module:

FileScope
avm.tflint.override.hclRoot module
avm.tflint_module.override.hclSubmodules
avm.tflint_example.override.hclExamples

Example:

# Disable the required resource id output rule β€” this is a pattern module.
rule "required_output_rmfr7" {
  enabled = false
}

Include a comment explaining why the rule is disabled.


Excluding examples from end-to-end testing

Create a file called .e2eignore in the example directory. Its contents should explain why the example is excluded from tests.


Global test setup and teardown

If your module requires setup/teardown across all examples, create:

  • examples/setup.sh β€” (optional) β€” runs before all examples.
  • examples/teardown.sh (optional) β€” runs after all examples.

These scripts are authorized with the same credentials as the examples.


Per-example pre and post scripts

For example-specific setup/teardown:

  • examples/<example_name>/pre.sh (optional) β€” runs before the example.
  • examples/<example_name>/post.sh (optional) β€” runs after the example.

These run in the context of the example directory, so relative paths work.


Repository governance PRs

A weekly workflow checks repository contents creates a PR if new files or updates are available. The PR is automatically merged, so there is no action required. Module owners will be informed if there are any one off PRs that require intervention.

These PRs do not change module code, so no new release is needed.


Eventual consistency

The Azure Resource Manager API can be eventually consistent. For example, data plane role assignments may not be available immediately after creation.

Use the AzAPI provider’s retry functionality to handle eventual consistency instead of arbitrary time_sleep delays. The AzAPI provider supports configurable retry with retry blocks that can match on specific error codes, providing a more reliable and efficient approach.

Repository Creation Process

Important

This page is for module owners only. If you are an external contributor, skip to the contribution flow.

Important

If this process is not followed exactly, it may result in your repository and any in-progress code being permanently deleted.

1. Add yourself to the Module Owners Team and Open Source orgs

If you have already completed these steps, skip to step 2.

  1. Open the Open Source Portal and ensure your GitHub account is linked to your Microsoft account.
  2. Open the Open Source Portal and ensure you are a member of the Azure and Microsoft organizations.
  3. Navigate to Core Identity and request access to the Azure Verified Module Owners Terraform entitlement.
Info

Until your entitlement request is approved, you can contribute by using JIT elevation.

2. Gather repository information

You’ll need the following from the module request issue:

InformationDescription
Module nameFormat: avm-<type>-<name> (e.g. avm-res-network-virtualnetwork)
Module owner GitHub handleYour GitHub handle
Module owner display nameFirstname Lastname
Module descriptionAuto-prefixed with Terraform Azure Verified <module-type> Module for ...
Resource provider namespaceResource modules only (e.g. Microsoft.Network)
Resource typeResource modules only (e.g. virtualNetworks)
Alternative namesOptional comma-separated list
Secondary owner handleOptional
Secondary owner display nameOptional

3. Create the repository

Prerequisites:

Clone and prepare

cd ~
git clone "https://github.com/Azure/avm-terraform-governance"
cd ./avm-terraform-governance/tf-repo-mgmt

Authenticate

# GitHub CLI
gh auth login -h "github.com" -w -p "https" -s "delete_repo" -s "workflow" -s "read:user" -s "user:email"

# Azure CLI (if not already logged in)
az login --scope https://graph.microsoft.com/.default --allow-no-subscriptions

Run the creation script

if(!(Test-Path -Path "./scripts/New-Repository.ps1")) {
    Write-Error "This script must be run from the tf-repo-mgmt directory."
    exit 1
}

# Required Inputs
$moduleProvider = "azurerm" # Allowed: azurerm, azapi, azure
$moduleName = "<module name>" # e.g. avm-res-network-virtualnetwork
$moduleDisplayName = "<module description>"
$resourceProviderNamespace = "" # Leave empty for Pattern/Utility modules
$resourceType = "" # Leave empty for Pattern/Utility modules
$ownerPrimaryGitHubHandle = "<github handle>"
$ownerPrimaryDisplayName = "<display name>"

# Optional
$moduleAlternativeNames = ""
$ownerSecondaryGitHubHandle = ""
$ownerSecondaryDisplayName = ""

./scripts/New-Repository.ps1 `
    -moduleProvider $moduleProvider `
    -moduleName $moduleName `
    -moduleDisplayName $moduleDisplayName `
    -resourceProviderNamespace $resourceProviderNamespace `
    -resourceType $resourceType `
    -ownerPrimaryGitHubHandle $ownerPrimaryGitHubHandle `
    -ownerPrimaryDisplayName $ownerPrimaryDisplayName `
    -moduleAlternativeNames $moduleAlternativeNames `
    -ownerSecondaryGitHubHandle $ownerSecondaryGitHubHandle `
    -ownerSecondaryDisplayName $ownerSecondaryDisplayName

Complete Open Source Portal Setup

The script will pause and prompt you to configure the Open Source Portal. Follow the link in the script output.

βž• If you see the Complete Setup link

Click Complete Setup and use the following settings:

QuestionAnswer
Classify the repositoryProduction
Assign a Service tree or Opt-outAzure Verified Modules / AVM
Direct ownersAdd yourself and jaredholgate or mawhi. Add avm-team-module-owners as fallback security group.
Public open source licensed project?Yes
What type of open source?Sample code
LicenseMIT
All code created by your team?Yes
Telemetry?Yes, telemetry
Cryptography?No
Project nameAzure Verified Module (Terraform) for ‘module name
Project version1
Project descriptionAzure Verified Module (Terraform) for ‘module name’. Part of AVM project - https://aka.ms/avm
Business goalsCreate IaC module accelerating Azure deployment using Microsoft best practice.
Used in a Microsoft product?Open source, can be leveraged in Microsoft services.
Security best practice?Yes, use just-in-time elevation
Maintainer / Write permissionsLeave empty
Repository template / .gitignoreUncheck both

Click Finish setup + start business review, then View repository, then Elevate your access.

βž• If you do NOT see the Complete Setup link
  1. Go to the Compliance tab and fill out:
    • Direct owners: Add yourself and jaredholgate or mawhi. Add avm-team-module-owners as fallback.
    • Classify the repository: Production
    • Service tree: Azure Verified Modules / AVM
  2. Go back to Overview and click Elevate your access if available.

Return to the terminal and type yes to complete repository configuration.

The script will automatically:

  • Create a PR to add module metadata to the avm-terraform-governance repository.
  • Create an issue to install the Azure Verified Modules GitHub App.

4. Update the issue status

  1. Add the Β Status: Repository Created πŸ“„Β  label.
  2. Remove the Β Status: Ready For Repository Creation πŸ“Β  label if present.

5. Wait for the GitHub App

Once installed (usually within 24 hours), the environment sync runs automatically at 15:30 UTC on weekdays to complete the repository setup.

Website Contribution Guide

Looking to contribute to the AVM Website, well you have made it to the right place/page. πŸ‘

Follow the below instructions, especially the pre-requisites, to get started contributing to the library.

Context/Background

Before jumping into the pre-requisites and specific section contribution guidance, please familiarize yourself with this context/background on how this library is built to help you contribute going forward.

This site is built using Hugo, a static site generator, that’s source code is stored in the AVM GitHub repo (link in header of this site too) and is hosted on GitHub Pages, via the repo.

The reason for the combination of Hugo & GitHub pages is to allow us to present an easy to navigate and consume library, rather than using a native GitHub repo, which is not easy to consume when there are lots of pages and folders. Also, Hugo generates the site in such a way that it is also friendly for mobile consumers.

But I don’t have any skills in Hugo?

That’s okay and you really don’t need them. Hugo just needs you to be able to author markdown (.md) files and it does the rest when it generates the site πŸ‘

Pre-Requisites

Read and follow the below sections to leave you in a “ready state” to contribute to AVM.

A “ready state” means you have a forked copy of the Azure/Azure-Verified-Modules repo cloned to your local machine and open in VS Code.

Run and Access a Local Copy of AVM Website During Development

When in VS Code you should be able to open a terminal and run the below commands to access a copy of the AVM website from a local web server, provided by Hugo, using the following address http://localhost:1313/Azure-Verified-Modules/:

cd docs
hugo server -D // you can add "--poll 700ms", if file changes are not detected

Software/Applications

To contribute to this website, you will need the following installed:

Tip

You can use winget to install all the pre-requisites easily for you. See the below section

  • Git
  • Visual Studio Code (VS Code)
    • Extensions:
      • editorconfig.editorconfig, streetsidesoftware.code-spell-checker, ms-vsliveshare.vsliveshare, medo64.render-crlf, vscode-icons-team.vscode-icons
      • VS Code will recommend automatically to install these when you open this repo, or a fork of it, in VS Code.
  • Hugo Extended

winget Install Commands

To install winget follow the install instructions here.

winget install --id 'Git.Git'
winget install --id 'Microsoft.VisualStudioCode'
winget install --id 'Hugo.Hugo.Extended'

Other requirements

Useful Resources

Below are links to a number of useful resources to have when contributing to AVM:

Steps to do before contributing anything (after pre-requisites)

Run the following commands in your terminal of choice from the directory where you fork of the repo is located:

git checkout main
git pull
git fetch -p
git fetch -p upstream
git pull upstream main
git push

Doing this will ensure you have the latest changes from the upstream repo, and you are ready to now create a new branch from main by running the below commands:

git checkout main
git checkout -b <YOUR-DESIRED-BRANCH-NAME-HERE>

Top Tips

Sometimes the local version of the website may show some inconsistencies that don’t reflect the content you have created

If this happens, simply kill the Hugo local web server by pressing CTRL + C and then restart the Hugo web server by running hugo server -D from the docs/ directory.

Experimental

Experimental Content - No Long-Term Support Guarantee

The content in this section represents experimental exploration of emerging technologies and innovative approaches. The AVM team makes no guarantees regarding:

  • Long-term support or maintenance of solutions described here
  • Future direction or adoption of these approaches into core AVM guidance
  • Stability or compatibility of experimental features over time

This section may evolve rapidly or be deprecated as technology and best practices advance.

About This Section

This section showcases the AVM team’s exploration of cutting-edge technologies and methodologies that have the potential to enhance Azure infrastructure development. The content here represents forward-thinking experiments and proof-of-concepts that we believe are valuable to share with the broader community.

What to Expect

Experimental by Nature: The solutions and approaches documented in this section are under active exploration. They may change significantly, be superseded by better alternatives, or be discontinued as we learn more about their practical applications and limitations.

Community Innovation: Some of the tooling, integrations, and methodologies featured here may be developed by community members, partners, or external teams rather than the AVM core team or AVM module owners. While we strive to ensure quality, these solutions may have different support models and maturity levels than core AVM content or AVM modules.

Shared Learning: By documenting and sharing these experimental approaches, we aim to foster innovation, gather feedback, and accelerate the discovery of what works well in real-world scenarios. Your experience and feedback with these experimental features are invaluable.

Your feedback is welcome!

We encourage you to try these experimental features and share your experiences. Your feedback helps us understand what resonates with the community and what might be worth developing further or integrating into core AVM guidance.

Provide feedback through GitHub Issues.

Use at Your Own Risk

Given the experimental nature of this content, we recommend:

  • Testing thoroughly in non-production environments before considering production use
  • Understanding the limitations and potential for breaking changes
  • Having contingency plans if an experimental feature is deprecated or significantly changed
  • Staying informed about updates and changes to experimental features you’re using

Explore the experimental features below to see what’s on the horizon for Azure IaC development with AVM!

Subsections of Experimental

AI-Assisted IaC Solution Development

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

For End Users

This section covers solution development scenarios and is primarily relevant for end users aiming to develop complex solutions based on AVM modules using AI. If you are a module owner looking for content on maintaining and developing AVM modules with AI assistance, see AI-Assisted Module Development.

This section explains concepts of developing IaC solution templates using Azure Verified Modules (AVM) with the assistance of AI tools such as GitHub Copilot and covers how AI-assisted development can accelerate the process of building and deploying Azure solutions using AVM modules.

This approach is referred to as AI-assisted IaC solution development rather than AI-driven or AI-led because humans remain fully in control of architectural decisions and solution design - AI simply handles the tedious, error-prone tasks of code generation and standards compliance, amplifying human ingenuity rather than replacing it.

AVM and GitHub Copilot

In the rapidly evolving landscape of AI, the convergence of Azure Verified Modules (AVM) and GitHub Copilot represents a transformative approach to building Azure solutions, addressing the fundamental challenges developers face: maintaining quality, consistency, and speed while navigating the complexity of modern cloud architectures.

How They Work Together

  1. AVM provides the foundational “knowledge base”: Comprehensive, standardized, continuously updated, pre-validated modules that embody Azure best practices, security standards, and architectural patterns. Each module undergoes rigorous testing and validation, ensuring reliability and compliance with Microsoft’s standards.
  2. GitHub Copilot brings AI-powered intelligence to the development workflow: AI-assisted code generation, module discovery, and real-time guidance based on AVM specifications with understanding both natural language intent and code context. Equipped with knowledge of AVM specifications and practices, Copilot serves as an expert guide that can:
    • Intelligently discover and recommend the appropriate AVM modules for your specific requirements
    • Generate compliant infrastructure code that adheres to AVM standards and Azure best practices
    • Accelerate development cycles by automating repetitive tasks and reducing manual lookups
    • Maintain consistency across your infrastructure codebase
    • Reduce errors by leveraging validated patterns and catching compliance issues early
  3. Developers focus on solution architecture: This approach enables teams to dedicate more time to designing solutions that meet business requirements, rather than working on repetitive tasks.

This synergy means developers can express their infrastructure needs in natural language, and Copilot translates these into production-ready IaC code using validated AVM modules, complete with appropriate configurations, security settings, and dependencies.

The Result

What once required hours of documentation review, module discovery, and manual code writing can now be accomplished in minutes. Infrastructure teams can iterate faster, maintain higher quality standards, and deliver Azure solutions with confidence, knowing they’re built on a foundation of verified, best-practice modules guided by AI technology.

The combination of human expertise, Azure Verified Modules, and AI assistance creates a new opportunity to transform how we build and deploy cloud solutions at scale.

Subsections of AI-Assisted IaC Solution Development

Spec Kit

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

Overview

Spec Kit is a reference implementation of Specification-Driven Development (SDD) principles, developed by members of the open-source community, including engineers from Microsoft and Anthropic. It provides a structured, AI-native workflow for translating project requirements into specifications, plans, executable tasks and implemented code - demonstrating how SDD concepts can be operationalized.

For detailed information (installation instructions, usage guidelines, etc.) visit the official repository: https://github.com/github/spec-kit

Example Scenario

In this chapter we’ll explore each step of the Spec Kit workflow in detail. We’ll use the following example to illustrate the process: using AVM modules, we need to develop a solution that will host a simple legacy application on a Windows virtual machine (VM). The solution must be secure and auditable. The VM must not be accessible from the internet and its logs should be easily accessible.

The Spec Kit Workflow

Spec Kit guides development teams through a systematic process that ensures specifications remain the single source of truth throughout the development lifecycle:


flowchart TB
    subgraph CAS["Constitution&nbsp;and&nbsp;Specification"]
        direction LR
        A["**Step 1**<br><code>/speckit.constitution</code><br>Establish project principles"] --> B["**Step 2**<br><code>/speckit.specify</code><br>Create baseline specification"]
        B --> C["**Step 3** <br><code>/speckit.clarify</code><br>(optional)<br>Ask structured questions"]
    end

    subgraph PAT["Implementation&nbsp;Plan&nbsp;and&nbsp;Tasks"]
        direction LR
        D["**Step 4** <br><code>/speckit.plan</code><br>Create implementation plan"] --> E["**Step 5** <br><code>/speckit.checklist</code><br>(optional)<br>Generate quality checklists"]
        E --> F["**Step 6** <br><code>/speckit.tasks</code><br>Generate actionable tasks"]
        F --> G["**Step 7** <br><code>/speckit.analyze</code><br>(optional)<br>Consistency & alignment report"]
    end

    subgraph IMP["Implementation"]
        H["**Step 8** <br><code>/speckit.implement</code><br>Execute<br>implementation"]
    end

    CAS --> PAT
    PAT --> IMP

    click A "#1-constitution"
    click B "#2-specify"
    click C "#3-clarify-optional"
    click D "#4-plan"
    click E "#5-checklist-optional"
    click F "#6-tasks"
    click G "#7-analyze-optional"
    click H "#8-implement"

    style C fill:#e1f5ff
    style E fill:#e1f5ff
    style G fill:#e1f5ff
    style CAS fill:#f5f5f5,stroke:#999999
    style PAT fill:#f5f5f5,stroke:#999999
    style IMP fill:#f5f5f5,stroke:#999999

Each command in the workflow works with a number of AI agents, including GitHub Copilot, enabling AI-assisted processing through the specification-to-implementation pipeline while maintaining human control over architectural decisions and project direction.

1. Constitution

The constitution document (constitution.md) establishes the governing principles, development guidelines and project-wide constraints that every later step (spec, plan, tasks, implementation) must follow. It acts as the “North Star” for the AI agent. It ensures that fundamental requirements are always met by encoding architectural, compliance, security, coding, and operational rules.

Spec Kit uses /speckit.constitution to generate the constitution.md file. The constitution can be evolved through iterating over the constitution.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.constitution.

The constitution typically includes:

  • Required engineering standards
  • Guardrails and constraints
  • Required / discouraged patterns
  • Organizational coding standards
  • Testing expectations
  • Cloud/platform governance
  • Naming conventions, tagging rules
  • Compliance/security requirements

Following our scenario, here are some examples for what the prompt used to generate the constitution should include:

  1. Architecture & Cloud Rules

    • Infrastructure must be declarative and written in Bicep only.
    • Must use Azure Verified Modules (AVM) for all supported resource types.
    • Deployment must follow a modular architecture using AVM best practices/standards.
  2. Security & Compliance

    • Enforce NSG rules, disable all incoming traffic from the internet, require JIT.
    • OS hardening baseline must be applied via VM extension or custom script.
    • All storage must have private endpoints and be encrypted using CMK if required.
  3. Ops & Reliability

    • Centralized logging via Azure Monitor / Log Analytics workspace.
    • Diagnostics extensions required for Windows Server.
  4. Naming & Tagging

    • All resources follow Microsoft Cloud Adoption Framework naming conventions.
    • Mandatory tags: owner, costCenter, env, application.
  5. Testing Expectations

    • Deployment must succeed in at least one Azure region with a Windows Server SKU supported by AVM (e.g., 2022 Datacenter).

2. Specify

The specification document (spec.md) is used to define the baseline specification for the product, focusing on “the what and the why” rather than the low-level technical requirements. It’s detached from the implementation, meaning, the same spec can be used even if the underlying technology changes.

The specification describes WHAT you want to build and WHY - not how. It is focused on user needs, functional requirements, constraints, and success criteria.

Spec Kit uses /speckit.specify to generate the spec.md file. Specifications can be evolved through iterating over the spec.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.specify and leveraging /speckit.clarify to review and challenge the specification.

The specification typically includes:

  • Feature description
  • User/problem statements
  • Functional and non-functional requirements
  • Scenarios & their constraints
  • Input/output expectations
  • Success criteria

Following our scenario, here are some examples for what the prompt used to generate the specification should include:

  1. What we are building

    • “A deployable IaC template, based on Azure Verified Modules, that provisions a secure single-VM environment suitable for hosting a legacy Windows Server-based line-of-business application.”
  2. Functional Requirements

    • Deploy all Azure resources through referencing the corresponding AVM module (if available).
    • Deploy required network components: VNet, Subnets, NSG, Bastion, optionally NAT gateway.
    • Install app binaries via Custom Script Extension.
    • Expose the application through an internal load balancer (if needed).
  3. Non-Functional Requirements

    • Must support repeatable deployments across environments (dev/test/prod).
    • Must comply with corporate security baselines.
  4. Constraints

    • Legacy application cannot be containerized.
    • Only one VM instance supported.
    • No direct internet access allowed.
  5. User Inputs

    • VM size, OS license type, admin credentials (via Key Vault), virtual network ranges.
  6. Success Criteria

    • VM deploys successfully and application runs.
    • Diagnostics logs are set to be collected.
    • Deployment is fully reproducible and parameterized.

3. Clarify (Optional)

The clarify step is used to identify missing information, ambiguity, contradictions, or incompleteness in the specification. It runs a structured Q&A loop where the AI agent asks questions derived from the spec and your constitution, ensuring high-quality requirements before planning. This step is crucial when the initial specification is incomplete or ambiguous - which is common for infra projects.

Spec Kit uses /speckit.clarify to generate adjust information captured in spec.md. The prompt doesn’t require any specific inputs as it analyzes the existing specification for gaps.

4. Plan

This step is where the technical requirements for the project are defined. Spec Kit consults the constitution to ensure that the non-negotiable principles are respected. The plan turns the specification into a technical architecture and concrete design. It defines how the system will be built - including technical stack and architecture choices. It also outlines the execution flow, primary dependencies, testing approaches, target platforms, and architecture principles.

Spec Kit uses /speckit.plan to generate the plan.md file. The plan can be evolved through iterating over the plan.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.plan, or leveraging /speckit.checklist to review/validate and challenge the plan.

The plan typically includes:

  • Selected technical stack & components
  • Architecture diagram or description
  • Component-level (resource-level) breakdown
  • API/data contracts (if any)
  • Integration points
  • Deployment structure
  • Region & SKU validation
  • Required research (limitations, assumptions)

Following our scenario, here are some examples for what the prompt used to generate the plan should include:

  1. Chosen Technologies

    • AVM Bicep or Terraform modules
  2. Architecture

    • Hub/spoke or simple VNet depending on environment.
    • Subnet for VM with NSG enforcing inbound/outbound rules.
    • Azure Bastion for private admin access.
    • VM uses Managed Identity for Key Vault + Storage access.
  3. Research (automatically generated)

    • Supported Windows Server SKUs in chosen region.
    • VM extension support for custom script on Windows.
    • Whether AVM module supports automatic patching, monitoring, identity binding.
  4. Data Inputs & Contracts

    • Parameters JSON structure for:
      • VM name, SKU
      • Admin username
      • Key Vault secret names
      • App package URI
  5. Project Structure

    • File and folder structure:
      main.bicep
      main.bicepparam
      readme.md
  6. Quickstart

    • How to implement/deploy: Steps to deploy via Azure CLI, Bicep CLI, PowerShell or GitHub Actions.

5. Checklist (Optional)

The checklist step ensures that all required criteria from the Constitution, Spec, and Plan are met or properly addressed before generating tasks or implementing.

Spec Kit uses /speckit.checklist to generate adjustments to the plan.md file. The prompt doesn’t require any specific inputs as it analyzes the existing plan for gaps.

6. Tasks

This step breaks the project work down into manageable and actionable chunks that the agent can tackle one by one. Tasks are the blueprint for coding/automation. For example, a plan can be broken down to individual phases and tasks, such as bootstrapping, test first (tests must fail before core implementation), core implementation, integration refinement, and polish/iterate.

Spec Kit uses /speckit.tasks to generate the tasks.md file. The prompt doesn’t require any specific inputs as it analyzes the existing plan to break it down into actionable tasks.

Tasks typically include:

  • Ordered, granular, developer-ready instructions, mapping to a single logical unit
  • Acceptance criteria per task
  • Notes for dependencies between tasks

Following our scenario, here are some examples for what the prompt used to generate the tasks should include:

  1. Solution Template Creation Tasks
    • Create base Bicep structure for the template.
    • Reference the AVM VNet module with parameterized address space.
    • Reference the AVM Subnet module and associate NSG.
    • Reference the AVM NSG module with rules (deny all inbound except required).
    • Reference the AVM VM module with parameters for:
      • VM size
      • OS SKU
      • Managed Identity
      • Boot diagnostics settings
    • Add VM Extension for custom script to install legacy application.
  2. Security Tasks
    • Integrate Key Vault references for secrets.
    • Apply security baseline hardening script.
  3. Observability Tasks
    • Add Log Analytics workspace and diagnostic settings.
  4. Documentation Tasks
    • Generate README with deployment instructions.
    • Generate sample parameter files for dev/test/prod.
  5. Testing Tasks
    • Write validation tests using What-If and PSRule for Azure.
    • Deploy to sandbox environment for verification.

7. Analyze (Optional)

The analyze step runs a final check for consistency, completeness, contradictions, and coverage across all generated artifacts - specification, plan, tasks - before implementation.

Spec Kit uses /speckit.analyze to generate an analysis report. The prompt doesn’t require any specific inputs as it analyzes the existing spec, plan and tasks to produce the report.

8. Implement

The implementation step is the final stage where Spec Kit executes all tasks, building the actual software solution by generating real code, scripts, documentation, tests, and supporting assets. The implementation strictly adheres to the guidelines and requirements set forth in the earlier stages (constitution, specification, plan, and tasks) to ensure consistency, quality, and alignment with the project’s goals.

Spec Kit uses /speckit.implement to implement all defined tasks by generating all code. The prompt doesn’t require any specific inputs as Spec Kit analyzes the existing plan and tasks to perform this step.

Following our scenario, here are some examples for what the output of the implementation step should include. Note that GitHub Copilot can actually deploy the generated main.bicep file to Azure, and validate whether the deployment was successful.

  1. Generated Code and Files

    • Bicep template (e.g., main.bicep)
    • Parameter file(s) (e.g., main.bicepparam)
    • Deployment scripts (if required)
    • Any additionally required configuration files
  2. Test Artifacts

    • Unit tests, integration tests, IaC linting results
    • What-If validation JSON
    • PSRule for Azure test configs
    • Regression tests (if in constitution)
  3. Documentation

    • README files
    • Architecture diagrams
    • Quick-start and deployment instructions
    • Known limitations and future improvements
  4. Tooling Outputs

    • Auto-created scaffolding (directories and files)
    • Code generation aligned to constraints

Summary

Spec Kit transforms the traditionally ambiguous process of translating requirements into code by establishing a structured, AI-assisted workflow. By following the steps - from Constitution through Implementation - you create a clear chain of traceability where every line of code can be linked back to a specific requirement.

Key takeaways:

  • Start with principles: The constitution ensures that non-negotiable constraints (security, compliance, architecture patterns) are embedded from the beginning.
  • Iterate on specifications: Use the clarify step to challenge assumptions and refine requirements before committing to implementation.
  • Plan before coding: A well-defined plan prevents costly rework and ensures alignment with AVM best practices.
  • Break down complexity: Tasks make large projects manageable and provide clear acceptance criteria for each unit of work.
  • Validate continuously: Use analyze and checklist steps to catch inconsistencies early in the process.

When combined with Azure Verified Modules, Spec Kit helps ensure that your infrastructure-as-code solutions are not only functional but also secure, maintainable, and aligned with Azure best practices.

For hands-on examples of using Spec Kit with AVM, see the Bicep Example guide.

Subsections of Spec Kit

AVM Example for Spec Kit

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

Prerequisites

You will need the following tools and components to complete this guide:

You will need the following tools and components to complete this guide:

Before you begin, make sure you have these tools installed in your development environment!

Solution Architecture

Before we begin coding, it is important to have details about what the infrastructure architecture will include. For our example, using AVM modules, we will be building a solution that will host a legacy business application running as a single Windows Server 2016 virtual machine (VM) with at least 2 CPU cores, 8GB RAM, Standard HDD OS disk, and a 500GB data disk.

The VM is accessible via Azure Bastion using secure RDP access (no public IP exposure). The solution needs an Azure Storage Account with an HDD-backed file share connected via private endpoint, and an Azure Key Vault to securely store the VM administrator password generated at deployment time. The VM must not be accessible from the internet, and all diagnostic logs will be captured in a Log Analytics workspace with critical alerts configured for VM availability, disk utilization, and Key Vault access failures.

Azure VM Solution Architecture

Bootstrapping

Tip

On a Windows PC, to get the uv package manager CLI tool required for locally installing the Specify CLI, run the following command:

winget install astral-sh.uv
  1. To install Spec Kit locally, run the following command in an elevated terminal:

    uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
  2. Create a new directory for your Spec Kit project and navigate into it - this folder ideally already exists as a git repository:

    mkdir avm-workload
    cd avm-workload
  3. Set your default branch to main (initializing Spec Kit will configure this working folder as a git repository if it isn’t one already):

    git config --global init.defaultBranch main
  4. Initialize a new Spec Kit project:

    specify init .

    If the folder has already been set up as a repository, the specify tool will warn you that the folder is not empty. Just confirm that you want to proceed.

    As we haven’t defined the AI assistant in our init command, specify will prompt us to choose one. Select copilot (GitHub Copilot) from the list. Similarly, as we haven’t defined the script type, specify will prompt us to choose one. Select ps (PowerShell) from the list.

    Alternatively, you can provide these parameters directly in the init command, like so:

    specify init . --ai copilot --script ps
    βž• Expand to see the results

    Note: As Spec Kit evolves, the output may change over time. This example is meant to give you an idea of what the user interface looks like.

    Click through the tabs to see the details!

    You should see something like this:
    Specify Bootstrap

    In your project folder, you should now see the following files and folders created by the specify tool:

    <!-- markdownlint-disable -->
    avm-workload
    β”‚
    β”œβ”€β”€β”€.github
    β”‚   β”œβ”€β”€β”€agents
    β”‚   β”‚       speckit.analyze.agent.md
    β”‚   β”‚       speckit.checklist.agent.md
    β”‚   β”‚       speckit.clarify.agent.md
    β”‚   β”‚       speckit.constitution.agent.md
    β”‚   β”‚       speckit.implement.agent.md
    β”‚   β”‚       speckit.plan.agent.md
    β”‚   β”‚       speckit.specify.agent.md
    β”‚   β”‚       speckit.tasks.agent.md
    β”‚   β”‚       speckit.taskstoissues.agent.md
    β”‚   β”‚
    β”‚   └───prompts
    β”‚           speckit.analyze.prompt.md
    β”‚           speckit.checklist.prompt.md
    β”‚           speckit.clarify.prompt.md
    β”‚           speckit.constitution.prompt.md
    β”‚           speckit.implement.prompt.md
    β”‚           speckit.plan.prompt.md
    β”‚           speckit.specify.prompt.md
    β”‚           speckit.tasks.prompt.md
    β”‚           speckit.taskstoissues.prompt.md
    β”‚
    β”œβ”€β”€β”€.specify
    β”‚   β”œβ”€β”€β”€memory
    β”‚   β”‚       constitution.md
    β”‚   β”‚
    β”‚   β”œβ”€β”€β”€scripts
    β”‚   β”‚   └───powershell
    β”‚   β”‚           check-prerequisites.ps1
    β”‚   β”‚           common.ps1
    β”‚   β”‚           create-new-feature.ps1
    β”‚   β”‚           setup-plan.ps1
    β”‚   β”‚           update-agent-context.ps1
    β”‚   β”‚
    β”‚   └───templates
    β”‚           agent-file-template.md
    β”‚           checklist-template.md
    β”‚           plan-template.md
    β”‚           spec-template.md
    β”‚           tasks-template.md
    β”‚
    β”œβ”€β”€β”€.vscode
    β”‚       settings.json
    β”‚
    └───infra
        β”œβ”€β”€β”€modules
        β”‚   β”œβ”€β”€β”€compute
        β”‚   β”œβ”€β”€β”€monitoring
        β”‚   β”œβ”€β”€β”€networking
        β”‚   β”œβ”€β”€β”€security
        β”‚   β”œβ”€β”€β”€shared
        β”‚   └───storage
        └───tests
            └───compliance
  5. Spec Kit automatically commits this step to the git repository with following comment: Initial commit from Specify template.

  6. The rest of the steps will be performed using GitHub Copilot Chat in VS Code: Start your VS Code environment, open or add the newly created folder to your workspace, and navigate to GitHub Copilot Chat using the dialog icon on the top of the window or by hitting CTRL+ALT+I.

Making it real

Spec Kit follows a structured workflow that guides you through each phase of solution development, from establishing foundational principles to implementing the final code. To learn more about Spec Kit, see the Spec Kit overview section.

flowchart LR
    A[1\. Constitution] --> B[2\. Specify]
    B --> C["3\. Clarify<br>(Optional)"]
    C --> D[4\. Plan]
    D --> E["5\. Checklist<br>(Optional)"]
    E --> F[6\. Tasks]
    F --> G["7\. Analyze<br>(Optional)"]
    G --> H[8\. Implement]

    click A "#1-constitution"
    click B "#2-specify"
    click C "#3-clarify-optional"
    click D "#4-plan"
    click E "#5checklist-optional"
    click F "#6-tasks"
    click G "#7-analyze-optional"
    click H "#8-implement"

    style C fill:#e1f5ff
    style E fill:#e1f5ff
    style G fill:#e1f5ff

To implement our example solution using AVM modules, we will walk through each of these steps in detail.

Each of the below steps will typically take 3-8 minutes to complete, depending on the complexity of your specification, the performance of the AI model you are using, and your reaction time to answer any outstanding questions and review and approve the generated content.

Choose your LLM

Changing the LLM does make a difference. We highly encourage you test different models to see which one works best for your needs.

Note: At the time of writing this article, we tested our prompts with Claude Sonnet 4.5 for Bicep and Claude Sonnet 4.6 for Terraform. In our experience, using Claude Opus 4.6 for Terraform typically leads to better, more accurate results, but also costs more tokens.

Know before you go
  1. As Spec Kit uses a set of built-in and system tools and scripts, you will need to approve the execution of each of these steps. Make sure you understand the impact of these commands before approving and proceeding!
    Here’s an example:
Specify Approve Scripts
  1. In some cases, your account might exceed GitHub’s API rate limits when using GitHub Copilot with Spec Kit. If that happens, please wait for a while (usually an hour or so) and try again.

1. Constitution

Spec Kit uses /speckit.constitution to generate the constitution.md file. The constitution can be evolved through iterating over the constitution.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.constitution.

Info

To learn more about what the constitution should include, see the Constitution chapter in the Spec Kit article.

βž• Before running /speckit.constitution (Expand)

Notice what the constitution.md file looks like before running the related prompt. It is just a template with placeholders, defining the structure:

Note: As Spec Kit evolves, the content of this template may change over time. This example is meant to give you an idea of what the starting point looks like.

<!-- markdownlint-disable -->
# [PROJECT_NAME] Constitution
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->

## Core Principles

### [PRINCIPLE_1_NAME]
<!-- Example: I. Library-First -->
[PRINCIPLE_1_DESCRIPTION]
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->

### [PRINCIPLE_2_NAME]
<!-- Example: II. CLI Interface -->
[PRINCIPLE_2_DESCRIPTION]
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args β†’ stdout, errors β†’ stderr; Support JSON + human-readable formats -->

### [PRINCIPLE_3_NAME]
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
[PRINCIPLE_3_DESCRIPTION]
<!-- Example: TDD mandatory: Tests written β†’ User approved β†’ Tests fail β†’ Then implement; Red-Green-Refactor cycle strictly enforced -->

### [PRINCIPLE_4_NAME]
<!-- Example: IV. Integration Testing -->
[PRINCIPLE_4_DESCRIPTION]
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->

### [PRINCIPLE_5_NAME]
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
[PRINCIPLE_5_DESCRIPTION]
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->

## [SECTION_2_NAME]
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->

[SECTION_2_CONTENT]
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->

## [SECTION_3_NAME]
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->

[SECTION_3_CONTENT]
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->

## Governance
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->

[GOVERNANCE_RULES]
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->

**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
  1. Run the following prompt to generate the constitution for our example:
/speckit.constitution Fill the constitution with the typical requirements of a legacy Azure workload (needed to be retained for compliance reasons; no high-availability requirements; no disaster recovery requirements; no scalability requirements), defined as infrastructure-as-code, in Bicep language, built only with Azure Verified Modules (AVM). Always try to implement every feature with Bicep first (using Infra-as-code), and only use custom scripts when it's not possible otherwise. Follow IaC best practices: define everything in a single template, and let ARM manage dependencies and the order of deployment for each Azure resource.

Security and reliability best practices must be followed under all circumstances.

The naming convention is to use just enough random characters to make the name unique and have the Azure resource type reflected in the name. Resource type specific character and length limitations must be respected.

Before running a deployment, always run a validation.

Deploy everything to the US West 3 datacenter region.
βž• Expand to see the results

The most important artifact created in this phase is the constitution.md file. Based on your inputs, additional files may also be created or updated, such as spec-template.md , plan-template.md, and tasks-template.md. These would further improve the quality of the generated artifacts in later phases.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Once Copilot finished running the prompt, you should see something like this in the Copilot chat area:

Constitution Chat Results

In the constitution.md file, notice, how changes implemented by Copilot are not final yet, until you review, and approve them by clicking on the “Keep” button. You can either do this on a paragraph-by-paragraph basis, or for the entire document at once by clicking on the “Keep” button in the main Copilot chat window.:

Constitution Before Approvals
<!-- markdownlint-disable -->
<!--
SYNC IMPACT REPORT
==================
Version Change: N/A β†’ 1.0.0 (Initial constitution ratification)

Modified Principles:
- N/A (Initial version)

Added Sections:
- Core Principles (6 principles defined)
- Infrastructure Standards
- Security & Compliance Requirements
- Development Workflow
- Governance

Removed Sections:
- N/A (Initial version)

Templates Requiring Updates:
- βœ… plan-template.md: Constitution Check section aligns with principles
- βœ… spec-template.md: Requirements structure aligns with security principles
- βœ… tasks-template.md: Task categorization aligns with validation-first workflow

Follow-up TODOs:
- None (all placeholders filled)

Change Rationale:
- MAJOR version (1.0.0) because this is the initial constitution establishing governance framework
- Principles focused on legacy workload characteristics: compliance retention, IaC-first with Bicep, AVM-only modules, validation-before-deployment
-->

# Legacy Azure Workload Constitution

## Core Principles

### I. Infrastructure-as-Code First (NON-NEGOTIABLE)
All Azure resources MUST be defined in Bicep templates. Manual Azure Portal configurations are STRICTLY PROHIBITED.

**Rationale**: Ensures compliance auditability, repeatability, and version control for regulatory requirements. Manual changes create configuration drift that violates compliance mandates.

**Rules**:
- Every feature starts with Bicep code defining the infrastructure
- Custom scripts are permitted ONLY when Bicep/ARM capabilities are insufficient
- All infrastructure changes MUST go through version control
- Single-template approach: define everything in one main.bicep, let ARM handle dependencies

### II. AVM-Only Modules
All Bicep infrastructure MUST use Azure Verified Modules (AVM). Direct resource declarations are permitted only when no AVM module exists.

**Rationale**: AVM modules are officially maintained, follow security best practices, and are compliance-tested by Microsoft.

**Rules**:
- Search for AVM module first (using `#list_avm_metadata` tool)
- Use latest stable version of AVM modules
- Document justification when direct resource declaration is necessary
- Follow AVM module documentation for parameter configuration

### III. Validation Before Deployment (NON-NEGOTIABLE)
Every deployment MUST be preceded by ARM validation. Deployments without prior validation are STRICTLY PROHIBITED.

**Rationale**: Prevents configuration errors that could impact compliance-required systems. Validation catches issues before they affect production.

**Rules**:
- Run `az deployment group validate` before every deployment
- Run `az deployment group what-if` to preview changes
- Document validation results in deployment logs
- Address all validation errors before proceeding

### IV. Security & Reliability First
Security and reliability best practices MUST be followed under all circumstances, even for legacy workloads.

**Rationale**: Compliance requirements mandate security controls regardless of workload age. Legacy status does not exempt from security obligations.

**Rules**:
- Enable Azure Monitor and diagnostic logs for all resources
- Apply network security groups and private endpoints where applicable
- Use managed identities instead of connection strings/keys
- Follow principle of least privilege for all access
- Enable Azure Security Center recommendations

### V. Minimal Naming with Type Identification
Resource names MUST be concise: minimal random characters for uniqueness + resource type identifier.

**Rationale**: Improves resource identification while respecting Azure naming limitations. Avoids verbose names that exceed character limits.

**Rules**:
- Format: `{resourceType}-{purpose}-{randomSuffix}`
- Example: `st-legacyvm-k7m3p` for storage account
- Respect Azure resource-specific length limits (e.g., storage: 24 chars, lowercase/numbers only)
- Random suffix: 4-6 alphanumeric characters
- Document naming pattern in infrastructure documentation

### VI. Region Standardization
All resources MUST deploy to US West 3 (westus3) region unless technically impossible.

**Rationale**: Centralizes resources for simplified management and cost tracking. Reduces complexity for legacy workloads with no multi-region requirements.

**Rules**:
- Default region parameter: `westus3`
- Document exceptions with technical justification
- Global resources (e.g., Azure Front Door) exempted by nature

## Infrastructure Standards

### Bicep Template Requirements
- Single main.bicep file as deployment entry point
- Use main.bicepparam for environment-specific parameters
- Leverage ARM dependency management (avoid explicit dependsOn unless necessary)
- Include detailed parameter descriptions and constraints
- Use Bicep decorators for validation (`@minLength`, `@maxLength`, `@allowed`)

### Module Management
- Reference AVM modules via Bicep Registry (br/public:avm/...)
- Pin to specific module versions (never use 'latest')
- Document module selection rationale in comments
- Review AVM module documentation for breaking changes during updates

### Documentation Requirements
- Maintain README.md with deployment instructions
- Document all parameters in main.bicepparam
- Include architecture diagram showing resource relationships
- Record compliance justifications for resource configurations

## Security & Compliance Requirements

### Mandatory Controls
- **Logging**: Enable diagnostic settings for all resources supporting it
- **Access Control**: Use Azure RBAC, no shared keys in parameters
- **Network Security**: Apply NSGs to subnet/NIC resources
- **Encryption**: Use Azure-managed encryption (minimum); customer-managed keys where compliance requires
- **Secrets Management**: Store sensitive values in Azure Key Vault, reference via Bicep getSecret()

### Compliance Documentation
- Tag all resources with compliance identifiers (e.g., `compliance: "legacy-retention"`)
- Document retention policies for data resources
- Record security exceptions with business justification
- Maintain audit trail of all infrastructure changes

### Prohibited Practices
- Hardcoded secrets or connection strings in Bicep files
- Public IP addresses without business justification
- Unrestricted network access (0.0.0.0/0 rules)
- Disabled diagnostic logging

## Development Workflow

### Pre-Deployment Phase
1. Research and select appropriate AVM modules
2. Draft Bicep templates with parameter documentation
3. Run local Bicep linting (`bicep build`)
4. Commit code to version control

### Validation Phase (MANDATORY GATE)
1. Run `az deployment group validate` and resolve all errors
2. Run `az deployment group what-if` and review changes
3. Document validation results
4. Obtain approval for resource changes (if required by organization)

### Deployment Phase
1. Deploy using validated parameters
2. Monitor deployment progress
3. Verify resource creation via Azure Portal/CLI
4. Test resource functionality
5. Document deployment outcomes

### Post-Deployment Phase
1. Verify diagnostic settings are active
2. Confirm tags applied correctly
3. Review security recommendations in Azure Security Center
4. Update documentation with deployed resource details

## Governance

This constitution supersedes all other development practices and guidelines. All infrastructure changes MUST comply with these principles.

### Amendment Process
1. Propose amendment with business/technical justification
2. Document impact on existing infrastructure
3. Update constitution with version increment following semantic versioning
4. Update all dependent templates and documentation
5. Communicate changes to all stakeholders

### Versioning Policy
- **MAJOR**: Breaking changes to core principles (e.g., removing AVM-only requirement)
- **MINOR**: New principle added or existing principle materially expanded
- **PATCH**: Clarifications, wording improvements, non-semantic fixes

### Compliance Review
All pull requests MUST verify compliance with this constitution. Constitution violations require explicit justification and approval exception.

Complexity that deviates from simplicity principles MUST be documented and justified with business or technical rationale.

**Version**: 1.0.0 | **Ratified**: 2026-01-27 | **Last Amended**: 2026-01-27
/speckit.constitution Fill the constitution with the typical requirements of a legacy Azure workload (needed to be retained for compliance reasons; no high-availability requirements; no disaster recovery requirements; no scalability requirements), defined as infrastructure-as-code, in Terraform language, built only with Azure Verified Modules (AVM). Always use Terraform, and never use custom scripts. Security and reliability best practices must be followed under all circumstances. Before running a deployment, always run a validation. Deploy everything to the US West 3 datacenter region.
βž• Expand to see the results

The most important artifact created in this phase is the constitution.md file. Based on your inputs, additional files may also be created or updated, such as spec-template.md , plan-template.md, and tasks-template.md. These would further improve the quality of the generated artifacts in later phases.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Once Copilot finished running the prompt, you should see something like this in the Copilot chat area:

Constitution Chat Results

In the constitution.md file, notice, how changes implemented by Copilot are not final yet, until you review, and approve them by clicking on the “Keep” button. You can either do this on a paragraph-by-paragraph basis, or for the entire document at once by clicking on the “Keep” button in the main Copilot chat window.:

Constitution Before Approvals
<!-- markdownlint-disable -->
<!--
Sync Impact Report - Version 1.0.0 (Initial Release)
Version: N/A β†’ 1.0.0 (MAJOR - Initial constitution establishment)
Change Type: Initial Release

Principles Defined:
  βœ… I. Terraform-First Infrastructure
  βœ… II. AVM-Only Modules (Terraform Registry)
  βœ… III. Security & Reliability (NON-NEGOTIABLE)
  βœ… IV. Single-Template Pattern (Terraform root module)
  βœ… V. Validation-First Deployment (terraform validate + plan)

Sections Added:
  βœ… Deployment Standards
  βœ… Project Constraints
  βœ… Naming Convention

Templates Status:
  ⚠ plan-template.md - Update Constitution Check for Terraform workflow (init β†’ validate β†’ plan β†’ apply)
  ⚠ spec-template.md - Update infrastructure requirements for .tf file patterns and state management
  ⚠ tasks-template.md - Update task patterns for Terraform validation, AVM module sourcing, and security scanning

Follow-up Actions:
  - Update plan-template.md Technical Context to include Terraform version and required providers
  - Ensure spec-template.md reflects Terraform state management and backend configuration requirements
  - Update tasks-template.md to include terraform init, validate, fmt, plan task patterns
  - Add Terraform-specific linting (tflint, tfsec, checkov) to setup phase tasks
  - Document Terraform backend configuration (Azure Storage for remote state) in project README
  - Add .terraform, .tfstate, .tfvars patterns to .gitignore
-->

# Azure Verified Modules Legacy Workload Constitution

## Core Principles

### I. Terraform-First Infrastructure
Every Azure resource MUST be defined as Infrastructure-as-Code in Terraform configuration files before any alternative approach is considered. Custom scripts (PowerShell, Azure CLI, Bash) are permitted ONLY when Terraform providers cannot accomplish the requirement.

**Rationale**: Declarative infrastructure ensures repeatability, version control, audit trails, and compliance documentation. Terraform's state management provides idempotent operations and built-in dependency resolution through resource graph analysis.

**Requirements**:
- All Azure resources declared in Terraform `.tf` files in project root or modules
- Single root module paradigm - let Terraform manage orchestration and dependencies via implicit resource references
- Custom scripts require explicit justification documenting why Terraform AzureRM provider cannot solve the need
- All infrastructure changes tracked in version control
- Terraform state stored remotely in Azure Storage Account with state locking enabled (blob container + lease)
- Use Terraform workspaces or separate state files for environment separation (dev, prod)
- `.terraform/`, `*.tfstate`, `*.tfstate.backup`, `*.tfvars` (except example files) excluded from version control

### II. AVM-Only Modules
All Terraform modules MUST be sourced exclusively from Azure Verified Modules (AVM) for Terraform. No custom or third-party Terraform modules are permitted unless an AVM module does not exist for the required resource type.

**Rationale**: AVM Terraform modules are Microsoft-maintained, tested against Azure best practices, include security hardening, follow consistent interfaces, and receive ongoing updates for provider changes and new Azure features.

**Requirements**:
- Use AVM resource modules (`Azure/avm-res-*`) from Terraform Registry for all supported Azure resources
- Use AVM pattern modules (`Azure/avm-ptn-*`) from Terraform Registry for multi-resource patterns
- Reference modules from official Terraform Registry source: `registry.terraform.io/Azure/avm-*`
- Pin module versions explicitly using pessimistic constraint (e.g., `version = "~> 0.1.0"`) - no floating latest versions
- If AVM module unavailable, document gap in ADR (Architecture Decision Record) and follow AVM authoring standards for local module
- Review and update AVM module versions quarterly minimum - document breaking changes in release notes review

**Example AVM Module Reference**:
```hcl
module "storage_account" {
  source  = "Azure/avm-res-storage-storageaccount/azurerm"
  version = "~> 0.1.0"

  # Module inputs per AVM interface
}
```

### III. Security & Reliability (NON-NEGOTIABLE)
Security and reliability best practices MUST be followed under all circumstances. This principle supersedes convenience, development velocity, and cost optimization.

**Rationale**: Legacy workloads retained for compliance reasons carry regulatory and legal obligations. Security breaches or reliability failures create compliance violations with potential legal ramifications.

**Requirements**:
- Managed identities required - no service principal credentials in Terraform code or variable files
- All secrets stored in Azure Key Vault - no plaintext secrets in `.tf`, `.tfvars`, or state files
- Use `sensitive = true` attribute for all secret outputs and variables
- Network security groups (NSGs) with explicit deny-by-default rules
- Azure Policy compliance validated before deployment (`terraform plan` must show policy compliance)
- Diagnostic settings and logging enabled on all supported resources (Activity Logs, Resource Logs)
- Resource locks (`CanNotDelete`) applied to prevent accidental deletion of compliance-critical resources
- Encryption at rest enabled (Microsoft-managed or customer-managed keys as appropriate)
- TLS 1.2+ required for all network communication (enforce via Azure Policy or resource properties)
- Principle of least privilege for all RBAC assignments (use built-in roles, no custom roles without justification)
- Static security analysis with `tfsec` or `checkov` in CI/CD pipeline - HIGH/CRITICAL findings block merge
- No hardcoded resource IDs or subscription IDs - use `data` sources or variables

### IV. Single-Template Pattern
All infrastructure for the workload MUST be defined in a single Terraform root module with Terraform managing dependencies and deployment order. Multi-stage deployments are permitted only when Terraform constraints (circular dependencies, provider limitations) make single-root infeasible.

**Rationale**: Single root module ensures atomic deployment, eliminates manual orchestration errors, simplifies rollback, and provides complete infrastructure visibility in one artifact. Terraform's dependency graph automatically determines execution order.

**Requirements**:
- One root module with `main.tf`, `variables.tf`, `outputs.tf`, and `versions.tf` (or combined files by preference)
- Use `depends_on` sparingly - rely on Terraform implicit dependency resolution via resource attribute references
- Child module composition for organizational clarity - all child modules instantiated in root module
- Separate `.tfvars` files for environment-specific values (e.g., `dev.tfvars`, `prod.tfvars`)
- If multi-stage required, document Terraform limitation necessitating split (rare - most circular dependencies solvable with proper design)
- No imperative orchestration scripts chaining multiple `terraform apply` commands

**Example Root Module Structure**:
```
terraform/
β”œβ”€β”€ main.tf           # Primary resource declarations and module calls
β”œβ”€β”€ variables.tf      # Input variable definitions
β”œβ”€β”€ outputs.tf        # Output value definitions
β”œβ”€β”€ versions.tf       # Terraform and provider version constraints
β”œβ”€β”€ backend.tf        # Remote state backend configuration
β”œβ”€β”€ dev.tfvars        # Development environment values
β”œβ”€β”€ prod.tfvars       # Production environment values
└── modules/          # Local child modules (only if AVM unavailable)
    └── custom/
```

### V. Validation-First Deployment
Every deployment MUST execute Terraform validation (`terraform validate`) and plan review (`terraform plan`) before actual apply. Deployments without successful validation and plan approval are prohibited.

**Rationale**: Validation catches syntax errors, type mismatches, and configuration issues. Plan preview catches logical errors, permission issues, policy violations, and unintended changes, preventing destructive actions and partial deployments.

**Requirements**:
- `terraform init` executed to initialize providers and backend
- `terraform fmt -check` executed to enforce code formatting (must pass before deployment)
- `terraform validate` executed and must return success before plan
- `terraform plan -out=plan.tfplan` executed for every deployment - output must be reviewed and approved
- Plan shows no unexpected resource deletions or replacements (unless explicitly intended and documented)
- Validation failures or unexpected plan changes block deployment pipeline - no manual override without incident review
- Plan file (`plan.tfplan`) stored as artifact in CI/CD for audit trail (encrypted if sensitive data present)
- Apply MUST use plan file: `terraform apply plan.tfplan` (no ad-hoc apply without plan)
- Plan diff documented in deployment logs for compliance audit trail

**Deployment Workflow**:
1. `terraform init -backend-config=backend-prod.hcl`
2. `terraform fmt -check -recursive`
3. `terraform validate`
4. `terraform plan -var-file=prod.tfvars -out=plan.tfplan`
5. **GATE**: Human review and approval of plan output
6. `terraform apply plan.tfplan`

## Deployment Standards

### Region & Availability
- **Target Region**: US West 3 (`westus3`) for all resources
- **High Availability**: Not required (legacy workload, compliance retention only)
- **Disaster Recovery**: Not required
- **Scalability**: Not required (fixed capacity sufficient)

**Rationale**: This is a legacy workload retained for compliance and legal record-keeping. Active user workloads have migrated to modern platforms. Fixed-region, single-instance deployments are appropriate and cost-effective.

**Terraform Implementation**:
- Use `location = "westus3"` for all resources (or variable `var.location` with default `"westus3"`)
- No zone redundancy, geo-replication, or auto-scaling configurations
- Accept default SKUs optimized for cost over high availability (e.g., Standard vs Premium)

### Naming Convention
Resource names MUST follow this pattern: `<resourceTypeAbbreviation>-<workloadName>-<randomSuffix>`

**Requirements**:
- Resource type abbreviation per [Azure naming best practices](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations) (e.g., `st` for Storage Account, `kv` for Key Vault, `vm` for Virtual Machine)
- Workload name: `avmlegacy` (or feature-specific descriptor)
- Random suffix: minimum characters needed for global uniqueness (e.g., 6-character alphanumeric generated via `random_string` resource)
- Respect Azure resource type character limits and restrictions:
  - Storage Account: 24 chars max, lowercase alphanumeric only, globally unique
  - Key Vault: 3-24 chars, alphanumeric and hyphens, globally unique
  - Resource Group: 1-90 chars, alphanumeric, underscores, hyphens, periods
- Use Terraform `random_string` or `random_id` resource to generate suffix consistently across deployments
- Document abbreviations in README or add comments in code

**Terraform Implementation Example**:
```hcl
resource "random_string" "unique_suffix" {
  length  = 6
  special = false
  upper   = false
}

locals {
  workload_name = "avmlegacy"
  location_abbr = "wus3"  # westus3 abbreviation

  # Resource names following convention
  storage_account_name = "st${local.workload_name}${random_string.unique_suffix.result}"  # max 24 chars
  key_vault_name       = "kv-${local.workload_name}-${random_string.unique_suffix.result}"
  resource_group_name  = "rg-${local.workload_name}-${local.location_abbr}"
}

resource "azurerm_resource_group" "main" {
  name     = local.resource_group_name
  location = "westus3"
}
```

**Examples**:
- `stavmlegacy8k3m9x` (Storage Account - 18 chars)
- `kv-avmlegacy-8k3m9x` (Key Vault - 20 chars)
- `rg-avmlegacy-wus3` (Resource Group)
- `vm-avmlegacy-001` (Virtual Machine - numeric suffix for multiple instances)

## Project Constraints

### Legacy Workload Context
This infrastructure supports a **legacy Azure workload** retained exclusively for compliance, regulatory, and legal record-keeping purposes. Active business operations have migrated off this system.

**Implications**:
- Cost optimization prioritized (single instances, no redundancy beyond Azure platform defaults)
- Change frequency low (quarterly patches and security updates only)
- User activity minimal (compliance audits, occasional data retrieval by legal/audit teams)
- Retention period defined by legal/compliance requirements (document separately in project README or ADR)
- Decommissioning planned when retention period expires (add sunset date if known)

**Terraform Impact**:
- Use smaller SKUs and tiers (Standard vs Premium) where compliance allows
- No auto-scaling configurations needed
- Simplified networking (single VNet, minimal subnets)
- Backup retention aligned with compliance requirements (not business continuity requirements)

### Non-Functional Requirements
- **Performance**: Adequate for infrequent access (no SLA requirements, no performance testing needed)
- **Availability**: Standard Azure platform availability (no custom HA configurations, 99.9% acceptable)
- **Capacity**: Fixed sizing (no auto-scaling, no capacity planning for growth)
- **Compliance**: MUST maintain audit logs (Azure Monitor Logs minimum 90 days), access controls (RBAC), data integrity (checksums, immutability where required)
- **Cost**: Target <$X/month (specify budget if known) - optimize for minimal operational cost
- **Maintenance Window**: Changes allowed during business hours (no 24/7 operations requirement)

## Governance

This constitution is the ultimate authority for all infrastructure decisions, architecture choices, and development practices for this workload. All team members, code reviews, and deployment pipelines MUST verify compliance.

### Amendment Process
1. Proposed changes documented with rationale and impact analysis (create ADR in `docs/decisions/` if significant)
2. Review by infrastructure lead and compliance officer
3. Approval required before amendment merge
4. Version incremented per semantic versioning:
  - **MAJOR**: Principle removal, redefinition, or backward-incompatible governance changes (e.g., switching IaC tools)
  - **MINOR**: New principle added or materially expanded guidance (e.g., adding new security requirement)
  - **PATCH**: Clarifications, corrections, non-semantic improvements (e.g., fixing typos, adding examples)
5. Migration plan required for MAJOR/MINOR changes affecting existing infrastructure (document in amendment PR)
6. All dependent templates and documentation updated atomically with constitution

### Compliance Review
- All pull requests MUST include constitution compliance checklist (see `.github/PULL_REQUEST_TEMPLATE.md`)
- Deployment pipelines MUST validate against principles where automatable:
  - Terraform fmt check (Principle I)
  - AVM module source validation (Principle II)
  - tfsec/checkov security scan (Principle III)
  - Plan approval gate (Principle V)
- Quarterly compliance audit against all principles with findings documented in team wiki/issue tracker
- Violations require remediation plan within 30 days or justified exception with expiration date (document in issue)

### Compliance Checklist (for PRs):
- [ ] All resources defined in Terraform (Principle I)
- [ ] All modules sourced from AVM Terraform Registry (Principle II)
- [ ] Security requirements met: managed identities, Key Vault, NSGs, logging (Principle III)
- [ ] Single root module pattern followed (Principle IV)
- [ ] Deployment includes terraform validate and plan review (Principle V)
- [ ] Naming convention followed (Deployment Standards)
- [ ] Resources deployed to westus3 region (Deployment Standards)
- [ ] No high-availability or disaster recovery features added unnecessarily (Project Constraints)

### Runtime Guidance
For day-to-day development guidance, coding standards, and tooling setup, refer to:
- `.specify/templates/` for specification and planning workflows
- Project `README.md` for Terraform setup, backend configuration, and deployment instructions
- `docs/` directory for additional Terraform patterns, troubleshooting, and Azure-specific guidance

**Version**: 1.0.0 | **Ratified**: 2026-02-18 | **Last Amended**: 2026-02-18
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture the new constitution of your project, with a comment of something like Constitution added.

2. Specify

Spec Kit uses /speckit.specify to generate the spec.md file. Specifications can be evolved through iterating over the spec.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.specify and leveraging /speckit.clarify to review and challenge the specification.

Info

To learn more about what the specification should include, see the Specification chapter in the Spec Kit article.

  1. Run the following prompt to generate the specification for our example:
/speckit.specify Create specification, called "01-my-legacy-workload" for a legacy business application, running as a single virtual machine connected to a virtual network. The VM must run Windows Server 2016, needs to have at least 2 CPU cores, 8 GB of RAM, a standard HDD, and a 500 GB HDD-based data disk attached. It must be remotely accessible via a bastion host and needs to have access to an HDD-backed file share in a storage account connected via a private endpoint. The VM must access the internet via a NAT gateway. Network Security Groups (NSGs) must be created for each subnet, configured and assigned as applicable, restricting traffic to only what's necessary. VM subnet NSG must allow inbound RDP (port 3389) from Bastion subnet to enable bastion connectivity.

The VM's administrator password (created at the time of deployment) must be stored in a Key Vault, also deployed as part of this solution. The VM's administrator account must be called "vmadmin". The VM's computer name (netbios name) must be 15 or fewer characters long.

Always rely on parameters from the main.bicepparam file only. Have the name of the secret used for the admin password captured as a parameter. Include rich comments in both the main.bicep and main.bicepparam files to explain the purpose of each resource and parameter.

When a decision needs to be made on availability zones, always choose a number between 1 and 3 (never choose -1, that explicitly disables this feature).

Create everything in a single resource group, standing for a production environment. Do not create any additional environments (such as dev, test, staging, etc.).

Read the documentation (readme.md file) of each module you need to use to find out what parameters and complex parameter objects you can use. Don't guess the allowed parameters.

The VM must not be accessible from the internet and its logs should be captured in Log Analytics, included with the solution. Configure diagnostic logging plus critical-only alerts (VM stopped, disk full, Key Vault access failures).
βž• Expand to see the results

Notice that the execution of the /speckit.specify created a new file called requirements.md and a file called spec.md in the specs/001-legacy-vm-workload/ folder.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Once Copilot finished running the prompt, you should see something like this in the Copilot chat area:

Specify Bootstrap Specify Bootstrap
<!-- markdownlint-disable -->
# Specification Quality Checklist: Legacy VM Workload Infrastructure

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-01-27
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Validation Results

**Status**: βœ… PASSED - All checklist items completed successfully

**Validation Date**: 2026-01-27

**Details**:
- Specification includes 5 user stories, properly prioritized (P1-P3)
- 16 functional requirements defined with concrete Azure resources
- 8 security & compliance requirements aligned with constitution
- 6 infrastructure constraints following project standards
- 5 monitoring & alerting requirements for operational visibility
- 11 success criteria, all measurable and deployment-focused
- Edge cases identified (6 scenarios)
- Assumptions documented (13 items)
- Out of scope clearly defined (13 items)
- No [NEEDS CLARIFICATION] markers - all requirements are concrete and actionable

**Readiness**: βœ… Specification is ready for `/speckit.plan` phase
<!-- markdownlint-disable -->
# Feature Specification: Legacy VM Workload Infrastructure

**Feature Branch**: `001-legacy-vm-workload`
**Created**: 2026-01-27
**Status**: Draft
**Input**: User description: "legacy business application, running as a single virtual machine connected to a virtual network with Windows Server 2016, 2 CPU cores, 8 GB RAM, standard HDD, 500 GB data disk, bastion access, file share via private endpoint, NAT gateway internet access, NSGs, Key Vault for VM password, Log Analytics with diagnostic logging and critical alerts"

## User Scenarios & Testing *(mandatory)*

<!--
  IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
  Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
  you should still have a viable MVP (Minimum Viable Product) that delivers value.

  Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
  Think of each story as a standalone slice of functionality that can be:
  - Developed independently
  - Tested independently
  - Deployed independently
  - Demonstrated to users independently
-->

### User Story 1 - Core VM Infrastructure Deployment (Priority: P1)

Deploy the fundamental infrastructure including virtual network, VM with required specifications, and basic connectivity. This establishes the baseline workload environment.

**Why this priority**: Without the VM and network infrastructure, no other components can function. This is the foundation for the entire workload.

**Independent Test**: Can be fully tested by deploying the infrastructure and verifying VM is created with correct specifications (Windows Server 2016, 2 cores, 8GB RAM, standard HDD) and can communicate within the VNet.

**Acceptance Scenarios**:

1. **Given** no existing infrastructure, **When** deployment is executed, **Then** VM is created with Windows Server 2016, 2 CPU cores, 8GB RAM
2. **Given** deployment is complete, **When** checking VM configuration, **Then** VM has standard HDD OS disk and is placed in correct VNet
3. **Given** VM is deployed, **When** checking computer name, **Then** NetBIOS name is 15 characters or fewer

---

### User Story 2 - Secure Storage and Data Disk (Priority: P2)

Provision the 500GB data disk for the VM and configure the storage account with file share accessible via private endpoint. This provides the data storage layer for the application.

**Why this priority**: Data storage is critical for application functionality but depends on the VM infrastructure being in place first.

**Independent Test**: Can be tested by verifying the 500GB HDD data disk is attached to the VM and the file share is accessible from the VM through the private endpoint.

**Acceptance Scenarios**:

1. **Given** VM infrastructure exists, **When** data disk deployment executes, **Then** 500GB HDD-based managed disk is attached to the VM
2. **Given** storage account is deployed, **When** checking storage configuration, **Then** HDD-backed file share is created
3. **Given** private endpoint is deployed, **When** VM attempts to access file share, **Then** connection succeeds through private network without traversing internet

---

### User Story 3 - Secure Access and Secrets Management (Priority: P2)

Implement bastion host for secure remote access and Key Vault for storing the VM administrator password. This ensures secure access patterns for operations teams.

**Why this priority**: Secure access is essential for ongoing operations but the infrastructure must exist before access can be configured.

**Independent Test**: Can be tested by connecting to the VM through bastion host using credentials retrieved from Key Vault.

**Acceptance Scenarios**:

1. **Given** bastion and Key Vault are deployed, **When** VM administrator password is generated at deployment, **Then** password is stored in Key Vault secret
2. **Given** bastion host is deployed, **When** operator attempts to connect to VM, **Then** connection succeeds through bastion without public IP on VM
3. **Given** Key Vault access is configured, **When** retrieving VM password, **Then** secret can be accessed only by authorized identities
4. **Given** VM subnet NSG is configured, **When** bastion attempts RDP connection to VM, **Then** traffic is allowed through NSG rule (port 3389 from Bastion subnet)

---

### User Story 4 - Internet Connectivity and Network Security (Priority: P3)

Configure NAT gateway for outbound internet access and implement Network Security Groups for all subnets with least-privilege rules.

**Why this priority**: Network security controls are important but the workload can function for testing without full NSG configuration initially.

**Independent Test**: Can be tested by verifying VM can reach internet through NAT gateway and that NSG rules block unauthorized traffic.

**Acceptance Scenarios**:

1. **Given** NAT gateway is deployed, **When** VM initiates outbound internet connection, **Then** traffic routes through NAT gateway
2. **Given** NSGs are configured, **When** unauthorized traffic attempts to reach VM, **Then** traffic is blocked by NSG rules
3. **Given** NSGs are deployed, **When** checking subnet associations, **Then** each subnet has appropriate NSG assigned

---

### User Story 5 - Monitoring and Alerting (Priority: P3)

Deploy Log Analytics workspace, configure diagnostic settings for all resources, and set up critical alerts (VM stopped, disk full, Key Vault access failures).

**Why this priority**: Monitoring is important for operations but the workload can function without it. It provides operational visibility rather than core functionality.

**Independent Test**: Can be tested by verifying diagnostic logs are flowing to Log Analytics and triggering test scenarios that generate alerts.

**Acceptance Scenarios**:

1. **Given** Log Analytics workspace is deployed, **When** resources are created, **Then** diagnostic settings send logs to workspace
2. **Given** alerting is configured, **When** VM is stopped, **Then** critical alert is triggered
3. **Given** alerting is configured, **When** Key Vault access fails, **Then** critical alert is triggered
4. **Given** diagnostic logging is active, **When** querying Log Analytics, **Then** logs are available within 5 minutes

---

## Clarifications

### Session 2026-01-27

- Q: VNet address space and subnet sizing for VM, bastion, and private endpoint subnets? β†’ A: VNet: 10.0.0.0/24, VM subnet: 10.0.0.0/27, Bastion subnet: 10.0.0.64/26, Private endpoint subnet: 10.0.0.128/27
- Q: Storage file share quota size? β†’ A: 1024 GiB (1 TiB)
- Q: Disk space alert threshold percentage? β†’ A: 85% full
- Q: Alert notification method for critical alerts? β†’ A: Azure Portal notifications only
- Q: VM size SKU for 2 cores and 8GB RAM requirement? β†’ A: Standard_D2s_v3

### Edge Cases

- What happens when VM computer name parameter would exceed 15 characters? (NetBIOS limit must be enforced)
- How does deployment handle when Key Vault secret name parameter is not provided or is invalid?
- What happens when storage account name would exceed 24 characters or contains invalid characters?
- How does system handle when no availability zone is specified for resources requiring zone selection?
- What happens when private endpoint deployment fails but storage account succeeds?
- How does deployment handle if bastion subnet already exists in the VNet from a previous deployment?

## Requirements *(mandatory)*

<!--
  ACTION REQUIRED: The content in this section represents placeholders.
  Fill them out with the right functional requirements.
-->

### Functional Requirements

<!-- NOTE: "Standard HDD" refers to Azure Standard_LRS disk SKU (magnetic disk storage) -->

- **FR-001**: Infrastructure MUST provision a Windows Server 2016 Virtual Machine with size Standard_D2s_v3 (2 vCPUs, 8 GiB RAM) using Standard HDD for OS disk
- **FR-002**: Infrastructure MUST attach a 500GB HDD-based managed disk to the VM as a data disk
- **FR-003**: Infrastructure MUST create Virtual Network with address space 10.0.0.0/24 containing three subnets: VM subnet (10.0.0.0/27), Bastion subnet (10.0.0.64/26), and private endpoint subnet (10.0.0.128/27)
- **FR-004**: Infrastructure MUST deploy Azure Bastion for secure remote access to the VM without public IP
- **FR-005**: Infrastructure MUST provision Storage Account with HDD-backed file share (1024 GiB quota) accessible via private endpoint
- **FR-006**: Infrastructure MUST deploy NAT Gateway for VM outbound internet connectivity
- **FR-007**: Infrastructure MUST create Network Security Groups for each subnet with least-privilege rules
- **FR-008**: Infrastructure MUST deploy Azure Key Vault to store VM administrator password
- **FR-009**: Infrastructure MUST set VM administrator account name to "vmadmin"
- **FR-010**: Infrastructure MUST ensure VM computer name (NetBIOS name) is 15 characters or fewer
- **FR-011**: Infrastructure MUST generate and store VM administrator password in Key Vault at deployment time
- **FR-012**: Infrastructure MUST accept Key Vault secret name as a parameter from main.bicepparam
- **FR-013**: Infrastructure MUST deploy all resources to a single resource group representing production environment
- **FR-014**: Infrastructure MUST select availability zone between 1-3 for zone-capable resources (never use -1)
- **FR-015**: Infrastructure MUST include rich comments in both main.bicep and main.bicepparam explaining resource purpose and parameters
- **FR-016**: Infrastructure MUST rely exclusively on parameters defined in main.bicepparam file

### Security & Compliance Requirements (Mandatory for all features)

- **SEC-001**: All resources MUST enable diagnostic settings and send logs to Log Analytics Workspace
- **SEC-002**: VM MUST use managed identity for Azure resource authentication (no connection strings/keys in configuration)
- **SEC-003**: Network Security Groups MUST restrict traffic to only necessary ports and protocols per subnet
- **SEC-003a**: VM subnet NSG MUST allow inbound RDP (port 3389) from Bastion subnet (10.0.0.64/26) to enable bastion connectivity
- **SEC-004**: All resources MUST be tagged with compliance identifier "legacy-retention"
- **SEC-005**: VM administrator password MUST be stored in Azure Key Vault, never in code or parameters
- **SEC-006**: VM MUST NOT have public IP address assigned (access only through bastion)
- **SEC-007**: Storage account file share MUST be accessible only through private endpoint, not public endpoint
- **SEC-008**: Key Vault MUST restrict access to only authorized identities using RBAC

### Infrastructure Constraints

- **IC-001**: MUST deploy to westus3 region (US West 3)
- **IC-002**: MUST use Azure Verified Modules (AVM) exclusively (read module readme.md for parameter documentation)
- **IC-003**: MUST validate deployment with `az deployment group validate` before applying
- **IC-004**: MUST run `az deployment group what-if` to preview changes
- **IC-005**: Resource names MUST follow pattern: {resourceType}-{purpose}-{random4-6chars}
- **IC-006**: MUST NOT create additional environments (dev, test, staging) - production only

### Monitoring & Alerting Requirements

- **MON-001**: Infrastructure MUST deploy Log Analytics workspace for centralized logging
- **MON-002**: Infrastructure MUST configure diagnostic logging for VM, Key Vault, Storage Account, and network resources
- **MON-003**: Infrastructure MUST create critical alert for VM stopped/deallocated condition (Portal notifications)
- **MON-004**: Infrastructure MUST create critical alert for disk space exceeding 85% threshold (Portal notifications)
- **MON-005**: Infrastructure MUST create critical alert for Key Vault access failures (Portal notifications)

### Key Azure Resources

- **Virtual Machine**: Windows Server 2016 VM with size Standard_D2s_v3 (2 vCPUs, 8 GiB RAM), Standard HDD OS disk, managed identity enabled
- **Managed Disk**: 500GB HDD-based data disk attached to VM
- **Virtual Network**: VNet with subnets for VM, bastion, and private endpoints
- **Azure Bastion**: Secure RDP access to VM without public IP
- **Storage Account**: Standard HDD storage with file share
- **Private Endpoint**: Secure connectivity between VM and storage account file share
- **NAT Gateway**: Outbound internet connectivity for VM subnet
- **Network Security Groups**: One per subnet with least-privilege rules
- **Key Vault**: Stores VM administrator password as secret
- **Log Analytics Workspace**: Centralized logging for all resources
- **Azure Monitor Alerts**: Critical alerts for VM stopped, disk full, Key Vault access failures

## Success Criteria *(mandatory)*

<!--
  ACTION REQUIRED: Define measurable success criteria.
  These must be technology-agnostic and measurable.
-->

### Measurable Outcomes

- **SC-001**: Infrastructure deploys successfully within 20 minutes including all resources
- **SC-002**: ARM validation (`az deployment group validate`) passes without errors
- **SC-003**: ARM what-if analysis shows all expected resources will be created
- **SC-004**: VM is accessible via bastion host within 5 minutes of deployment completion
- **SC-005**: VM can access file share through private endpoint connection
- **SC-006**: VM can reach internet through NAT gateway for outbound connections
- **SC-007**: Diagnostic logs from all resources appear in Log Analytics within 5 minutes
- **SC-008**: All resources pass Azure Security Center baseline compliance checks
- **SC-009**: NSG rules successfully block unauthorized traffic in test scenarios
- **SC-010**: VM administrator password can be retrieved from Key Vault by authorized identities
- **SC-011**: Critical alerts can be triggered and verified (VM stop, simulated disk full warning, Key Vault access attempt)

## Assumptions

- Azure subscription has sufficient quota for Standard_D2s_v3 VM size
- Azure Bastion service is available in westus3 region
- Windows Server 2016 image is available in Azure Marketplace for westus3 region
- Log Analytics workspace can be deployed in westus3 region
- Private endpoint feature is available for storage accounts in westus3 region
- NAT Gateway is available in westus3 region
- Deployment is executed by identity with sufficient permissions to create all resource types
- Resource group name will be provided as parameter in main.bicepparam
- Random suffix for resource names will be generated or provided as parameter
- Default log retention period of 30 days is acceptable for Log Analytics (compliance requirement may differ)
- Standard_LRS storage redundancy is acceptable for this legacy workload
- VM will be deployed without availability sets or scale sets (single instance acceptable)
- Availability zone selection (1, 2, or 3) will be provided as parameter

## Out of Scope

- Multi-region deployment or disaster recovery configuration
- High availability (availability sets, load balancers, multiple VMs)
- Auto-scaling capabilities
- Backup and restore automation (Azure Backup configuration)
- Additional environments (development, test, staging)
- Application installation or configuration on the VM
- Custom monitoring dashboards or complex alerting logic beyond critical alerts
- Network connectivity to on-premises networks (VPN or ExpressRoute)
- Azure Active Directory domain join
- Additional data disks beyond the single 500GB disk specified
- Storage account configuration beyond file share (no blob containers, tables, or queues)
- Advanced network features (Azure Firewall, Application Gateway, Traffic Manager)
- Cost optimization recommendations or reserved instance planning
/speckit.specify Create specification, called "01-my-legacy-workload" for a legacy business application, running as a single virtual machine connected to a virtual network. The VM must run Windows Server 2016, needs to have at least 2 CPU cores, 8 GB of RAM, a standard HDD, and a 500 GB HDD-based data disk attached. It must be remotely accessible via a bastion host and needs to have access to an HDD-backed file share in a storage account connected via a private endpoint. The VM must access the internet via a NAT gateway. Network Security Groups (NSGs) must be created for each subnet, configured and assigned as applicable, restricting traffic to only what's necessary. VM subnet NSG must allow inbound RDP (port 3389) from Bastion subnet to enable bastion connectivity.

The VM's administrator password (created at the time of deployment) must be stored in a Key Vault, also deployed as part of this solution. The VM's administrator account must be called "vmadmin". The VM's computer name (netbios name) must be 15 or fewer characters long.

Always rely on values from the `terraform.tfvars` file only. Have the name of the secret used for the admin password captured as a variable. Include rich comments in both the `main.tf` and `terraform.tfvars` files to explain the purpose of each resource and variable.

When a decision needs to be made on availability zones, always choose a number between 1 and 3 (never choose -1, that explicitly disables this feature).

Create everything in a single resource group, standing for a production environment. Do not create any additional environments (such as dev, test, staging, etc.).

Read the documentation (readme.md file) of each module you need to use to find out what variables and complex variable objects you can use. Don't guess the allowed variables.

The VM must not be accessible from the internet and its logs should be captured in Log Analytics, included with the solution. Configure diagnostic logging plus critical-only alerts (VM stopped, disk full, Key Vault access failures).

The Azure resource naming convention should follow Azure Cloud Adoption Framework guidance. Resource type specific character and length limitations must be respected. Random character should only be added to resources that must be globally unique like storage accounts. All resource names should be kebab case unless the hyphen is not supported for that resource.
βž• Expand to see the results

Notice that the execution of the /speckit.specify created a new file called requirements.md and a file called spec.md in the specs/001-legacy-vm-workload/ folder.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Once Copilot finished running the prompt, you should see something like this in the Copilot chat area:

Specify Bootstrap Specify Bootstrap
# Specification Quality Checklist: Legacy Business Application Infrastructure

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-18
**Feature**: [001-my-legacy-workload/spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Validation Notes

### Content Quality Assessment
- **βœ… Pass**: Specification focuses on infrastructure requirements (WHAT) without specifying HOW to implement (e.g., specific Terraform syntax, module parameters beyond identifying which AVM modules to use)
- **βœ… Pass**: Written for infrastructure stakeholders and operations teams - describes Azure resources, security controls, and operational requirements
- **βœ… Pass**: All mandatory sections present: User Scenarios & Testing, Requirements, Success Criteria, Assumptions, Dependencies, Out of Scope

### Requirement Completeness Assessment
- **βœ… Pass**: Zero [NEEDS CLARIFICATION] markers - all requirements fully specified based on user input
- **βœ… Pass**: Requirements are testable - each FR and SEC item can be verified (e.g., "VM MUST have 2+ CPU cores" - check VM properties; "NSG MUST allow RDP from Bastion only" - check NSG rules)
- **βœ… Pass**: Success criteria are measurable with specific metrics (e.g., "deployment completes within 30 minutes", "RDP connection established within 2 minutes", "cost under $200/month")
- **βœ… Pass**: Success criteria avoid implementation details - focus on outcomes (e.g., "VM can mount file share" not "private endpoint DNS configuration works")
- **βœ… Pass**: Acceptance scenarios defined for all 4 user stories with Given/When/Then format
- **βœ… Pass**: Edge cases identified (8 scenarios covering naming limits, zone availability, DNS resolution, secret conflicts, NSG rules, storage naming, subnet sizing, disk attachment)
- **βœ… Pass**: Scope clearly bounded with detailed "Out of Scope" section (15 items explicitly excluded like HA config, DR, multiple environments, domain join, etc.)
- **βœ… Pass**: Dependencies section lists all prerequisites (Terraform version, Azure CLI, AVM modules, Azure subscription,state backend, etc.)
- **βœ… Pass**: Assumptions section documents all implicit decisions (15 assumptions covering quotas, state backend existence, permissions, naming conflicts, zone support, application compatibility, etc.)

### Feature Readiness Assessment
- **βœ… Pass**: All 25 functional requirements map to user stories and have testable acceptance criteria
- **βœ… Pass**: 4 user stories cover complete infrastructure lifecycle: P1 (core compute/network), P2 (secure access), P3 (storage), P4 (internet/monitoring)
- **βœ… Pass**: Each user story independently testable and deliverable
- **βœ… Pass**: Success criteria define 13 measurable outcomes aligned with requirements
- **βœ… Pass**: No Terraform syntax or module-specific parameters in spec (appropriate - those belong in plan/implementation phase)

### Specification Completeness: READY FOR PLANNING βœ…

**Summary**: Specification passes all quality gates. No clarifications needed. Ready to proceed with `/speckit.plan` to generate implementation plan.

**Recommended Next Steps**:
1. Run `/speckit.plan` to generate implementation plan with Terraform architecture
2. During planning phase, research AVM module documentation for each identified resource
3. Define Technical Context (Terraform version, provider versions, AVM module versions)
4. Create Project Structure (terraform/ directory layout)
5. Generate tasks.md with phased implementation (Setup β†’ Foundational β†’ US1-US4 β†’ Polish)
# Feature Specification: Legacy Business Application Infrastructure

**Feature Branch**: `001-my-legacy-workload`
**Created**: 2026-02-18
**Status**: Draft
**Input**: User description: "Legacy business application running as a single virtual machine with Windows Server 2016, networking (VNet, Bastion, NAT Gateway, NSGs), storage (file share via private endpoint), Key Vault for secrets, and Log Analytics for monitoring"

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Core Compute and Network Infrastructure (Priority: P1)

Deploy a Windows Server 2016 virtual machine within an isolated virtual network with proper subnet segmentation and network security controls. This provides the foundational compute and network infrastructure required for the legacy application.

**Why this priority**: Without the VM and basic networking, the application cannot run. This is the minimum viable infrastructure that delivers compute capability in an isolated, secure network environment.

**Independent Test**: Deploy Terraform configuration, verify VM is created and running in the specified VNet with proper subnets. Confirm NSGs are attached to subnets and default deny rules are in place. VM should be isolated with no internet or external access at this stage.

**Acceptance Scenarios**:

1. **Given** Terraform configuration with VM and VNet resources, **When** `terraform apply` is executed, **Then** a Windows Server 2016 VM is created with 2+ CPU cores, 8GB RAM, standard HDD OS disk, and 500GB HDD data disk
2. **Given** the VM is deployed, **When** checking the virtual network, **Then** VNet contains at least 3 subnets (VM subnet, Bastion subnet, Private Endpoint subnet) with appropriate CIDR ranges
3. **Given** subnets are created, **When** checking NSG assignments, **Then** each subnet has an NSG attached with deny-by-default rules configured
4. **Given** VM is running, **When** checking VM properties, **Then** computer name is 15 characters or fewer, administrator account is "vmadmin", and password is stored in Key Vault (secret name configurable)
5. **Given** VM subnet NSG rules, **When** checking inbound rules, **Then** NSG allows RDP (port 3389) from Bastion subnet only

---

### User Story 2 - Secure Remote Access (Priority: P2)

Enable secure remote access to the virtual machine through Azure Bastion and store the VM administrator password securely in Azure Key Vault. This allows administrators to manage the VM without exposing it to the internet.

**Why this priority**: Secure access is critical for managing the VM and performing administrative tasks. Without Bastion, the VM would need a public IP (violating security requirements) or would be completely inaccessible.

**Independent Test**: After deploying US1, deploy Bastion and Key Vault infrastructure. Verify administrators can connect to the VM via Azure Bastion using credentials retrieved from Key Vault. Confirm VM has no public IP address.

**Acceptance Scenarios**:

1. **Given** Bastion host and Key Vault are deployed, **When** administrator navigates to VM in Azure Portal, **Then** "Connect via Bastion" option is available
2. **Given** Bastion connection initiated, **When** using "vmadmin" username and password from Key Vault, **Then** RDP session to VM is successfully established
3. **Given** Key Vault is deployed, **When** checking secrets, **Then** VM administrator password is stored as a secret with configurable name (defined in terraform.tfvars)
4. **Given** VM networking configuration, **When** checking VM properties, **Then** VM has no public IP address and is not directly accessible from internet
5. **Given** Key Vault access policies, **When** Terraform deploys the infrastructure, **Then** Key Vault uses managed identity for authentication (no service principal credentials in code)

---

### User Story 3 - Application Storage Integration (Priority: P3)

Provide secure access to an Azure Files share for application data storage, connected via private endpoint to ensure data does not traverse the public internet.

**Why this priority**: The legacy application requires access to a file share for data persistence. This enables the application's core functionality while maintaining security through private connectivity.

**Independent Test**: After deploying US1 and US2, deploy storage account with file share and private endpoint. From the VM, mount the Azure Files share using private endpoint IP. Verify data can be written to and read from the share without public internet connectivity.

**Acceptance Scenarios**:

1. **Given** storage account is deployed, **When** checking storage configuration, **Then** storage account has HDD-backed file share created (Standard tier, not Premium)
2. **Given** private endpoint is deployed, **When** checking network connectivity, **Then** private endpoint is connected to the Private Endpoint subnet in the VNet
3. **Given** private endpoint exists, **When** checking DNS resolution from VM, **Then** storage account FQDN resolves to private endpoint IP address (not public IP)
4. **Given** VM is running, **When** attempting to mount file share, **Then** file share is accessible from VM using private IP and SMB protocol
5. **Given** storage NSG rules, **When** checking Private Endpoint subnet NSG, **Then** NSG allows SMB traffic (port 445) from VM subnet only

---

### User Story 4 - Internet Access and Observability (Priority: P4)

Enable outbound internet access via NAT Gateway for Windows Updates and patches, and implement comprehensive monitoring through Log Analytics with diagnostic logging and critical alerts.

**Why this priority**: While not required for basic application functionality, internet access enables the VM to download security updates. Monitoring and alerting provide operational visibility and compliance evidence.

**Independent Test**: After deploying US1-US3, deploy NAT Gateway and Log Analytics. From VM, verify outbound internet connectivity (e.g., download Windows Update). Confirm diagnostic logs are flowing to Log Analytics and test alerts trigger correctly.

**Acceptance Scenarios**:

1. **Given** NAT Gateway is deployed and associated with VM subnet, **When** VM attempts outbound HTTP/HTTPS connection, **Then** connection succeeds with traffic routed through NAT Gateway
2. **Given** VM attempts inbound connection from internet, **When** traffic reaches VM subnet, **Then** connection is blocked (VM remains inaccessible from internet)
3. **Given** Log Analytics workspace is deployed, **When** checking diagnostic settings, **Then** VM, Key Vault, and Storage Account have diagnostic logging enabled sending logs to Log Analytics
4. **Given** alerts are configured, **When** VM is stopped, **Then** critical alert notification is triggered
5. **Given** alerts are configured, **When** VM disk reaches 90% capacity, **Then** critical alert notification is triggered
6. **Given** alerts are configured, **When** Key Vault access failure occurs (e.g., permission denied), **Then** critical alert notification is triggered

---

### Edge Cases

- **VM naming constraints**: What happens when generated VM computer name exceeds 15 characters (NetBIOS limit)? Truncate or error during validation.
- **Availability zone selection**: If availability zones 1-3 are unavailable in westus3 region, how does deployment handle this? Fail with clear error or fall back to no-zone deployment?
- **Private endpoint DNS**: What happens when private DNS zone for storage account doesn't exist or isn't linked to VNet? File share mount will fail without proper DNS resolution.
- **Key Vault secret naming**: What happens when the secret name specified in terraform.tfvars already exists in Key Vault? Overwrite or error?
- **NSG rule conflicts**: What happens when custom NSG rules conflict with required rules (e.g., accidentally blocking RDP from Bastion)? Terraform should fail validation.
- **Storage account naming**: What happens when randomly generated storage account name conflicts with existing global namespace? Terraform apply will fail - require retry with new random suffix.
- **Bastion subnet size**: What happens when VNet address space is too small for required subnets including /26 for Bastion? Deployment fails with clear CIDR allocation error.
- **Managed disk attachment**: What happens when 500GB data disk fails to attach to VM? Deployment should fail atomically (VM should not be left in inconsistent state).

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: Infrastructure MUST deploy a Windows Server 2016 virtual machine with minimum 2 CPU cores and 8GB RAM
- **FR-002**: VM MUST use standard HDD for OS disk (not SSD) for cost optimization
- **FR-003**: VM MUST have a 500GB HDD-based managed disk attached as data disk
- **FR-004**: VM computer name (NetBIOS name) MUST be 15 characters or fewer to comply with Windows naming limits
- **FR-005**: VM administrator account MUST be named "vmadmin"
- **FR-006**: VM administrator password MUST be generated at deployment time and stored in Key Vault
- **FR-007**: Infrastructure MUST deploy a virtual network with at least 3 subnets (VM subnet, Bastion subnet, Private Endpoint subnet)
- **FR-008**: Infrastructure MUST deploy Network Security Groups for each subnet with deny-by-default posture
- **FR-009**: VM subnet NSG MUST allow inbound RDP (port 3389) from Bastion subnet ONLY
- **FR-010**: Infrastructure MUST deploy Azure Bastion for secure RDP access to VM (no public IP on VM)
- **FR-011**: VM MUST NOT be accessible directly from the internet (no public IP address on VM)
- **FR-012**: Infrastructure MUST deploy NAT Gateway associated with VM subnet for outbound internet access
- **FR-013**: Infrastructure MUST deploy storage account with HDD-backed (Standard tier) Azure Files share
- **FR-014**: Storage account file share MUST be accessible from VM via private endpoint (no public access)
- **FR-015**: Infrastructure MUST deploy Key Vault for storing VM administrator password securely
- **FR-016**: Key Vault secret name for VM password MUST be configurable via terraform.tfvars variable
- **FR-017**: Infrastructure MUST use managed identities for authentication (no service principal credentials in Terraform code)
- **FR-018**: Infrastructure MUST deploy Log Analytics workspace for centralized logging
- **FR-019**: VM, Key Vault, and Storage Account MUST have diagnostic settings enabled sending logs to Log Analytics
- **FR-020**: Infrastructure MUST configure critical alerts: VM stopped, VM disk usage >90%, Key Vault access failures
- **FR-021**: All configurable values (VM size, disk sizes, subnet CIDRs, secret names, etc.) MUST be defined in terraform.tfvars (not hardcoded in main.tf)
- **FR-022**: All Terraform files (main.tf, terraform.tfvars) MUST include rich comments explaining purpose of each resource, variable, and configuration block
- **FR-023**: When selecting availability zones, MUST choose zone 1, 2, or 3 (NEVER use -1 or no-zone unless region doesn't support zones)
- **FR-024**: All resources MUST be deployed in a single resource group representing production environment
- **FR-025**: Infrastructure MUST reference AVM module documentation (readme.md) to determine correct variable names and object structures (no guessing)

### Infrastructure Requirements *(for Terraform IaC projects)*

**Azure Resources Required**:
- **Resource 1**: Resource Group for all resources - Built-in: `azurerm_resource_group`
- **Resource 2**: Virtual Network with 3+ subnets - AVM Module: `Azure/avm-res-network-virtualnetwork/azurerm`
- **Resource 3**: Network Security Groups (3 minimum) - AVM Module: `Azure/avm-res-network-networksecuritygroup/azurerm`
- **Resource 4**: Windows Server 2016 Virtual Machine - AVM Module: `Azure/avm-res-compute-virtualmachine/azurerm`
- **Resource 5**: Azure Bastion Host - AVM Module: `Azure/avm-res-network-bastionhost/azurerm`
- **Resource 6**: Key Vault for password storage - AVM Module: `Azure/avm-res-keyvault-vault/azurerm`
- **Resource 7**: Storage Account with file share - AVM Module: `Azure/avm-res-storage-storageaccount/azurerm`
- **Resource 8**: Private Endpoint for storage - AVM Module: `Azure/avm-res-network-privateendpoint/azurerm` (or part of storage module)
- **Resource 9**: NAT Gateway - AVM Module: `Azure/avm-res-network-natgateway/azurerm`
- **Resource 10**: Log Analytics Workspace - AVM Module: `Azure/avm-res-operationalinsights-workspace/azurerm`
- **Resource 11**: Alerts/Action Groups - AVM Module: `Azure/avm-res-insights-actiongroup/azurerm` and alert resources
- **Resource 12**: Random string for naming suffix - Built-in: `random_string` from random provider

**Infrastructure Constraints**:
- **IC-001**: All resources MUST be deployed to `westus3` region (per constitution)
- **IC-002**: Terraform state MUST be stored in Azure Storage backend with state locking enabled
- **IC-003**: Resource naming MUST follow `<type>-<workload>-<suffix>` pattern (per constitution, e.g., `vm-avmlegacy-8k3m9x`)
- **IC-004**: No high-availability or geo-redundancy configurations (legacy workload constraint, single VM acceptable)
- **IC-005**: All resources MUST be deployed in single Resource Group (named per convention: `rg-my-legacy-workload-prod-wus3`)
- **IC-006**: Availability zone MUST be selected (zone 1, 2, or 3) - NEVER use -1 or disable zones explicitly
- **IC-007**: Use HDD/Standard tier storage for cost optimization (OS disk, data disk, storage account)
- **IC-008**: VNet MUST have sufficient address space for minimum 3 subnets (recommend /23 or larger for VNet, /26 for Bastion per Azure requirements)
- **IC-009**: Private Endpoint subnet MUST have `privateEndpointNetworkPolicies` disabled per Azure requirements
- **IC-010**: Computer name generation MUST enforce 15-character limit (Windows NetBIOS constraint)

**Security & Compliance**:
- **SEC-001**: VM MUST use managed identity (system-assigned) for Azure service authentication
- **SEC-002**: VM administrator password MUST be stored in Key Vault with secret name defined in terraform.tfvars
- **SEC-003**: NO service principal credentials or secrets in Terraform files (.tf or .tfvars)
- **SEC-004**: Network Security Groups MUST implement deny-by-default posture with explicit allow rules only
- **SEC-005**: VM subnet NSG MUST allow RDP (3389) ONLY from Bastion subnet CIDR (source IP restricted)
- **SEC-006**: Bastion subnet NSG MUST allow inbound 443 from internet (Azure Bastion requirement) and outbound RDP to VM subnet
- **SEC-007**: Private Endpoint subnet NSG MUST allow SMB (445) from VM subnet for file share access
- **SEC-008**: VM MUST NOT have public IP address (internet inaccessible)
- **SEC-009**: Storage account MUST have public network access disabled (private endpoint only)
- **SEC-010**: Diagnostic logging MUST be enabled on VM, Key Vault, and Storage Account sending logs to Log Analytics
- **SEC-011**: Resource locks (CanNotDelete) MUST be applied to Resource Group, VM, Key Vault, and Storage Account (compliance-critical resources)
- **SEC-012**: Key Vault MUST have soft-delete and purge protection enabled
- **SEC-013**: Storage account MUST use encryption at rest with Microsoft-managed keys (minimum)
- **SEC-014**: All network traffic between VM and storage MUST traverse private endpoint (verified via NSG flow logs or connection test)

**State Management**:
- **State Backend**: Azure Storage Account in separate resource group (pre-existing, not created by this Terraform)
- **State File**: `my-legacy-workload-prod.tfstate`
- **State Locking**: Enabled via blob lease mechanism
- **Workspaces/Key Prefix**: Single production environment only - use `prod.tfvars` for variable values

### Key Entities *(include if feature involves data)*

- **Virtual Machine**: Windows Server 2016 compute instance running legacy business application. Attributes: computer name (≀15 chars), size (Standard_D2s_v3 or similar with 2+ cores, 8GB RAM), OS disk (Standard HDD), data disk (500GB Standard HDD), administrator credentials (username: vmadmin, password in Key Vault).

- **Virtual Network**: Isolated network containing all infrastructure. Attributes: address space (e.g., 10.0.0.0/23), subnets (VM subnet, Bastion subnet /26, Private Endpoint subnet).

- **Network Security Group**: Firewall rules for subnet-level traffic control. Attributes: associated subnet, inbound rules (RDP from Bastion, SMB to private endpoint), outbound rules (allow NAT Gateway for internet, deny all else).

- **Azure Bastion**: Managed PaaS service providing secure RDP/SSH access. Attributes: Bastion subnet (/26 minimum), public IP (managed by Bastion), SKU (Basic or Standard).

- **Key Vault**: Secure secrets store for VM password. Attributes: soft-delete enabled, purge protection enabled, access policies (Terraform managed identity for deployment, VM managed identity for runtime access if needed).

- **Storage Account**: Azure Files storage for application data. Attributes: Standard performance tier (HDD), LRS replication, file share (e.g., 100GB quota), private endpoint connection.

- **Private Endpoint**: Network interface in VNet providing private IP for storage account. Attributes: subnet (Private Endpoint subnet), private DNS integration (optional but recommended).

- **NAT Gateway**: Managed outbound internet gateway. Attributes: public IP address, associated with VM subnet for outbound traffic.

- **Log Analytics Workspace**: Centralized log repository. Attributes: retention period (e.g., 30-90 days per compliance requirements), diagnostic settings (VM, Key Vault, Storage Account).

- **Alerts**: Monitoring rules triggering on critical conditions. Attributes: VM stopped alert, disk space alert (>90%), Key Vault access failure alert, action group for notifications.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: Infrastructure deployment completes successfully via `terraform apply` within 30 minutes with all resources in "healthy" state
- **SC-002**: Administrator can establish RDP connection to VM via Azure Bastion within 2 minutes of infrastructure deployment completion
- **SC-003**: VM administrator password retrieved from Key Vault successfully authenticates RDP session via Bastion (100% success rate)
- **SC-004**: VM can mount Azure Files share via private endpoint and read/write files without errors (verified via test file operations)
- **SC-005**: VM can download content from internet via NAT Gateway (e.g., successful Windows Update check or HTTP GET to microsoft.com)
- **SC-006**: VM is NOT reachable via direct internet connection (verified via external port scan showing no open ports)
- **SC-007**: Diagnostic logs from VM, Key Vault, and Storage Account appear in Log Analytics within 15 minutes of deployment
- **SC-008**: Critical alerts (VM stopped, disk >90%, Key Vault access failure) trigger notifications within 5 minutes of condition occurring (tested via controlled failure scenarios)
- **SC-009**: All Terraform validation steps pass (`terraform fmt -check`, `terraform validate`, `tfsec` with no HIGH/CRITICAL findings)
- **SC-010**: Infrastructure deployment uses ONLY values from terraform.tfvars (no hardcoded values in main.tf) - verified via code review
- **SC-011**: All AVM module variables are correctly structured per module documentation (verified by successful deployment without Terraform errors)
- **SC-012**: Computer name is 15 characters or fewer (verified via VM properties after deployment)
- **SC-013**: Total monthly cost of deployed infrastructure is under $200/month (estimated based on Azure pricing calculator)

## Assumptions

- **A-001**: Azure subscription has sufficient quota for VM size, NAT Gateway, and Bastion in westus3 region
- **A-002**: Terraform state backend (Azure Storage Account) already exists and is configured prior to deployment (not managed by this Terraform code)
- **A-003**: Deployment identity (user, service principal, or managed identity running Terraform) has Contributor access to target subscription or resource group scope
- **A-004**: No existing resources with conflicting names in the subscription (e.g., duplicate storage account name, Key Vault name)
- **A-005**: Azure region westus3 supports availability zones (deployment assumes zone selection 1-3 is valid)
- **A-006**: Legacy application compatibility with Windows Server 2016 has been validated (not part of infrastructure scope)
- **A-007**: Legacy application does not require specific VM size beyond minimum 2 cores, 8GB RAM (actual VM SKU chosen for cost-performance balance)
- **A-008**: 500GB data disk is sufficient for application data storage requirements (not dynamically scaled)
- **A-009**: Standard HDD performance is adequate for legacy application workload (no IOPS/throughput requirements specified)
- **A-010**: File share quota (e.g., 100GB) is sufficient for application needs - configurable in terraform.tfvars if different
- **A-011**: Alert notifications can use email or webhook action group (specific notification target configured separately or in variables)
- **A-012**: VNet address space 10.0.0.0/23 (512 IPs) is sufficient and does not conflict with on-premises networks or VPN peering requirements
- **A-013**: No ExpressRoute or VPN gateway integration required (VM internet access is outbound only via NAT Gateway)
- **A-014**: VM does not require domain join (workgroup/standalone configuration acceptable)
- **A-015**: No existing Azure Policy assignments block required configuration (e.g., policy preventing public IP on Bastion, policy requiring specific encryption)

## Dependencies

- **D-001**: Terraform >= 1.5.0 installed on deployment machine
- **D-002**: Azure CLI authenticated with sufficient permissions (`az login` completed)
- **D-003**: AVM modules available from Terraform Registry (internet connectivity required during `terraform init`)
- **D-004**: Azure subscription with active valid payment method and sufficient credits
- **D-005**: Pre-existing Azure Storage Account and container for Terraform state backend
- **D-006**: Pre-existing Resource Group for Terraform state backend (separate from workload resource group)
- **D-007**: Azure region westus3 supports all required resource types (VM, Bastion, NAT Gateway, availability zones)
- **D-008**: AVM module documentation (readme.md) accessible for each module used (refer to Terraform Registry or GitHub)
- **D-009**: Terraform providers: azurerm (~> 3.75), random (~> 3.5) (specified in versions.tf)
- **D-010**: Security scanning tools (tfsec or checkov) installed if enforcing constitution security requirements
- **D-011**: Windows Server 2016 image available in Azure Marketplace (standard Microsoft image)
- **D-012**: Understanding of Terraform module composition (reading AVM module documentation to determine input variables and complex objects)

## Out of Scope

- **OS-001**: Installation or configuration of legacy business application on the VM (infrastructure only, app deployment separate)
- **OS-002**: Domain join or Active Directory integration (standalone/workgroup VM)
- **OS-003**: VPN gateway or ExpressRoute connectivity to on-premises networks
- **OS-004**: Multiple environments (dev, test, staging) - ONLY production environment deployed
- **OS-005**: High availability configuration (multiple VMs, load balancer, availability set) - single VM by design per legacy constraint
- **OS-006**: Disaster recovery or geo-replication configuration (no backup policies, no secondary region)
- **OS-007**: Auto-scaling or dynamic resource sizing (fixed VM size, fixed disk sizes)
- **OS-008**: Custom VM extensions or DSC configuration (beyond basic deployment)
- **OS-009**: Application-level monitoring or APM (only infrastructure-level diagnostics in Log Analytics)
- **OS-010**: Database deployment (if legacy app uses database, assumed to be on separate server or managed service)
- **OS-011**: DNS records in public or private DNS zones (beyond private endpoint DNS if AVM module handles it)
- **OS-012**: Certificate management or SSL/TLS termination (application responsibility if needed)
- **OS-013**: Cost management tags beyond basic workload identification (detailed cost center, project, owner tags)
- **OS-014**: Compliance frameworks implementation (HIPAA, PCI-DSS, SOC2) - basic security controls only per constitution
- **OS-015**: Terraform module development (using existing AVM modules only, no custom module authoring)
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture the clarified specification of your project, with a comment of something like Specification created.

3. Clarify (Optional)

Spec Kit uses /speckit.clarify to generate adjust information captured in spec.md. The prompt doesn’t require any specific inputs as it analyzes the existing specification for gaps.

Info

To learn more about the clarify step, see the Clarify chapter in the Spec Kit article.

  1. Run the following prompt to generate clarification questions for our example:
/speckit.clarify
βž• Expand to see example questions

The clarify phase iterates on the spec.md file by asking questions, making suggestions and capturing the user’s feedback.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

When running the clarify prompt, Copilot may ask you a number of depth questions to clarify certain aspects of the plan. Here’s an example of what that looks like. You can answer in the following format, e.g.: Q1: E, Q2:A, Q3:A
Specify Bootstrap

In the Copilot chat window, you will likely see some questions raised, similar to these. You can answer these just like in a normal chat conversation - e.g., by typing the letter standing for the option provided for each question, or by elaborating further if needed.

Specify Bootstrap Specify Bootstrap Specify Bootstrap

See a few examples of clarifying questions Copilot may ask. Copilot typically suggests a few options, but you can always deviate from them as needed, just use the chat to provide your answers.

## Clarifications

- Q: How should the Azure file share be mounted on the Windows VM? β†’ A: Post-deployment manual mount by administrator following documented procedure (no automation, aligns with IaC-first principle)
- Q: What level of monitoring and alerting should be configured for this legacy workload? β†’ A: Diagnostic logging plus critical-only alerts (VM stopped, disk full, Key Vault access failures)
- Q: If the initial deployment partially fails (e.g., VM creates but Bastion fails), what should the recovery procedure be? β†’ A: Keep existing resources, fix errors in template/parameters, redeploy entire template (ARM incremental mode handles already-deployed resources)
- Q: File share initial quota and growth strategy? β†’ A: 1TB initial quota with documented growth monitoring procedure
- Q: VM administrator username? β†’ A: vmadmin
- Q: VNet address space and subnet sizing for VM, bastion, and private endpoint subnets? β†’ A: VNet: 10.0.0.0/24, VM subnet: 10.0.0.0/27, Bastion subnet: 10.0.0.64/26, Private endpoint subnet: 10.0.0.128/27
- Q: Storage file share quota size? β†’ A: 1024 GiB (1 TiB)
- Q: Disk space alert threshold percentage? β†’ A: 85% full
- Q: Alert notification method for critical alerts? β†’ A: Azure Portal notifications only
- Q: VM size SKU for 2 cores and 8GB RAM requirement? β†’ A: Standard_D2s_v3
/speckit.clarify
βž• Expand to see example questions

The clarify phase iterates on the spec.md file by asking questions, making suggestions and capturing the user’s feedback.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

When running the clarify prompt, Copilot may ask you a number of depth questions to clarify certain aspects of the plan. Here’s an example of what that looks like. You can answer in the following format, e.g.: Q1: E, Q2:A, Q3:A
Specify Bootstrap

In the Copilot chat window, you will likely see some questions raised, similar to these. You can answer these just like in a normal chat conversation - e.g., by typing the letter standing for the option provided for each question, or by elaborating further if needed.

Specify Bootstrap Specify Bootstrap Specify Bootstrap Specify Bootstrap Specify Bootstrap

See a few examples of clarifying questions Copilot may ask. Copilot typically suggests a few options, but you can always deviate from them as needed, just use the chat to provide your answers.

## Clarifications

- Q: Backup & Recovery Strategy - Does the legacy workload require Azure Backup for VM and data disk? β†’ A: No backups needed - VM and data disk are disposable, can be recreated from Terraform
- Q: VNet Address Space and Subnet Sizing - What specific CIDR allocations should be used for the 3 required subnets? β†’ A: Minimal: 10.0.0.0/24 (VM: /27, Bastion: /26, PrivateEndpoint: /28) - tight fit, no growth
- Q: VM Size SKU Selection - Which specific Azure VM SKU should be used for the 2-core/8GB requirement? β†’ A: Standard_D2s_v3 (General Purpose) - balanced, widely used, predictable performance
- Q: Azure Files Share Quota and Performance Tier - What provisioned capacity should the file share have? β†’ A: 1TB Standard tier (LRS) - large capacity for growth
- Q: Log Analytics Workspace Retention Period - How long should diagnostic logs be retained? β†’ A: 180 days retention - extended compliance coverage, moderate cost increase
- Q: How should the Azure file share be mounted on the Windows VM? β†’ A: Post-deployment manual mount by administrator following documented procedure (no automation, aligns with IaC-first principle)
- Q: What level of monitoring and alerting should be configured for this legacy workload? β†’ A: Diagnostic logging plus critical-only alerts (VM stopped, disk full, Key Vault access failures)
- Q: If the initial deployment partially fails (e.g., VM creates but Bastion fails), what should the recovery procedure be? β†’ A: Keep existing resources, fix errors in template/parameters, redeploy entire template (ARM incremental mode handles already-deployed resources)
- Q: VM administrator username? β†’ A: vmadmin
- Q: VNet address space and subnet sizing for VM, bastion, and private endpoint subnets? β†’ A: VNet: 10.0.0.0/24, VM subnet: 10.0.0.0/27, Bastion subnet: 10.0.0.64/26, Private endpoint subnet: 10.0.0.128/27
- Q: Disk space alert threshold percentage? β†’ A: 85% full
- Q: Alert notification method for critical alerts? β†’ A: Azure Portal notifications only
  1. Review and approve the changes suggested by Copilot by clicking on the “Keep” button!
  2. It is recommended to make a commit now to capture the updated specification of your project, with a comment of something like Specification clarified.

4. Plan

Spec Kit uses /speckit.plan to generate the plan.md file. The plan can be evolved through iterating over the plan.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.plan, or leveraging /speckit.checklist to review/validate and challenge the plan.

Info

To learn more about what the plan should include, see the Plan chapter in the Spec Kit article.

Click through the tabs to see the details!

  1. Run the following prompt to generate the plan for our example:
/speckit.planΒ Create a detailed plan for the spec. Build with the latest version of Bicep and the latest available version of each AVM module. Use the "Bicep/list_avm_metadata" MCP tool to find out what's the latest version of each module. Only include direct resource references in the Bicep template if no related AVM resource modules are available. Similarly, for diagnostic settings, role assignments, resource locks, tags, managed identities, private endpoints, customer manged keys, etc., always use the related "interface" built-in to each resource module when available. Do not create and reference local modules, or any other bicep files.Β If a subset of the deployments fail, don't delete anything, just attempt redeploying the whole solution after fixing any bugs. Create a single main.bicep file, with direct references to AVM modules and leverage a single *.bicepparam file for all input parameters.

When generating the admin password for the VM, use the secret feature built into the AVM Key Vault module. Leverage the uniqueString function to generate a new random password and do not use any external helper script (including deployment scripts) for generating the password. Provide this password to the VM module by referencing the Key vault secret that stores it. The template must first generate this password including a random, complex string, using the uniqueString Bicep function, store it in Key Vault and then reference it for the VM to use it as admin password at deployment time.

Don't connect the file share to the VM just yet - i.e., no need to extract storage keys or shared access signatures - we will do this later.

If implementing resource level locks, always use the built-in AVM "interface" for resource locks, instead of directly deploying the "Microsoft.Authorization/locks" resource.

Bicep template must compile without warnings or errors using the latest stable Bicep CLI version. Create a bicepconfig.json file to generate a warning when not the latest version of an AVM module is used. You can configure this by making sure in the bicepconfig.json file, there is a node under analyzers/core/rules/use-recent-module-versions/level" with the value of "warning". Before validating the template or attempting the first deployment, always fix all warnings or errors related to the AVM module versioning by updating to the latest available version of each module.
βž• Expand to see the results

Notice how the plan step creates the plan.md file and a number of additional helper files. These may very depending on your prompts, the solution you are building, the version of Spec Kit and the LLM used. These typically include: data-model.md, research.md, quickstart.md and optional files in the contracts folder, such as outputs.md and parameters.md.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see results, similar to this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Implementation Plan: Legacy VM Workload

**Branch**: `001-legacy-vm-workload` | **Date**: 2026-01-27 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-legacy-vm-workload/spec.md`

**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.

## Summary

Deploy a legacy Windows Server 2016 virtual machine workload to Azure using Infrastructure-as-Code (Bicep) with the following capabilities:

- **Core Infrastructure**: Windows Server 2016 VM (Standard_D2s_v3) in availability zone 1, with system-assigned managed identity, 500GB HDD data disk, no public IP, deployed in dedicated VNet (10.0.0.0/24)
- **Secure Access**: Azure Bastion for RDP access, password stored in Key Vault, no direct internet exposure
- **Storage**: 1TB Azure Files share accessible via private endpoint from VM subnet
- **Network Security**: NAT Gateway for outbound internet, NSGs on all subnets with least-privilege rules, private endpoint for storage account
- **Monitoring**: Log Analytics workspace with diagnostic settings on all resources, three critical alerts (VM stopped, disk space >85%, Key Vault access failures), portal-only notifications
- **Naming**: Minimal naming convention with resource type prefix and 4-6 character random suffix
- **Region**: All resources in westus3 (US West 3)

**Technical Approach**: Single-template Bicep deployment using 12 Azure Verified Modules (latest stable versions), ARM-managed dependencies, parameter-driven configuration with uniqueString() for password generation.

## Technical Context

**IaC Language**: Bicep v0.33.0 or later (latest stable)
**Module Framework**: Azure Verified Modules (AVM) - 12 modules identified
**Target Region**: westus3 (US West 3)
**Deployment Tool**: Azure CLI v2.65.0+ (`az deployment group create`)
**Validation Required**: `bicep build` + `az deployment group validate` + `what-if` analysis
**Workload Type**: Legacy compliance-retained workload (Windows Server 2016)
**High Availability**: Single-zone deployment (availability zone parameter: 1, 2, or 3)
**Disaster Recovery**: Not required
**Scalability Requirements**: Static single VM, no auto-scaling
**Security Baseline**: Diagnostic logging to Log Analytics, managed identities, NSGs, private endpoints, Key Vault for secrets
**Naming Convention**: `{resourceType}-{purpose}-{random4-6chars}` (e.g., `vm-legacyvm-k7m3p`)
**Compliance Tags**: `workload: legacy-vm`, `environment: production`, `compliance: legacy-retention`

### AVM Modules Selected (Latest Versions)

| Module | Version | Purpose |
|--------|---------|---------|
| avm/res/network/virtual-network | 0.7.2 | VNet with 3 subnets (VM, Bastion, PE) |
| avm/res/compute/virtual-machine | 0.21.0 | Windows Server 2016 VM with data disk |
| avm/res/network/bastion-host | 0.8.2 | Secure RDP access |
| avm/res/storage/storage-account | 0.31.0 | File share with private endpoint |
| avm/res/network/nat-gateway | 2.0.1 | Outbound internet connectivity |
| avm/res/network/network-security-group | 0.5.2 | Subnet-level network security (3 NSGs) |
| avm/res/key-vault/vault | 0.13.3 | Store VM admin password |
| avm/res/operational-insights/workspace | 0.15.0 | Centralized logging |
| avm/res/network/private-endpoint | 0.11.1 | Private storage access |
| avm/res/insights/metric-alert | 0.4.1 | Monitoring alerts (3 alerts) |
| avm/res/network/private-dns-zone | 0.8.0 | DNS for private endpoints |

**Module Documentation**: See [research.md](./research.md) for detailed module analysis and alternatives considered.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- [x] **I. Infrastructure-as-Code First**: βœ… All resources defined in single `main.bicep` template, no manual Portal configurations, deployment via Azure CLI only
- [x] **II. AVM-Only Modules**: βœ… Using 12 AVM modules with latest stable versions (0.7.2 to 2.0.1), no direct resource declarations except where AVM unavailable (none identified)
- [x] **III. Validation Before Deployment**: βœ… Quickstart guide includes `bicep build`, `az deployment group validate`, and `what-if` analysis steps before deployment
- [x] **IV. Security & Reliability First**: βœ… Diagnostic logs to Log Analytics for all resources, managed identities (VM system-assigned), NSGs on all subnets, private endpoint for storage, Key Vault for password, least-privilege RBAC
- [x] **V. Minimal Naming with Type ID**: βœ… Naming pattern: `{type}-{purpose}-{random}` (e.g., `vm-legacyvm-k7m3p`, `kv-legacyvm-k7m3p`), random suffix via `uniqueString(resourceGroup().id)` - 6 chars
- [x] **VI. Region Standardization**: βœ… All resources deploy to westus3, parameter default set to `'westus3'`, no exceptions needed

**Constitution Compliance Status**: **PASSED** βœ…

All 6 constitution principles satisfied. No violations requiring justification.

## Project Structure

### Documentation (this feature)

## Project Structure

### Documentation (this feature)

```text
specs/001-legacy-vm-workload/
β”œβ”€β”€ spec.md              # Feature specification (completed)
β”œβ”€β”€ plan.md              # This file - implementation plan (in progress)
β”œβ”€β”€ research.md          # AVM module research (Phase 0 - completed)
β”œβ”€β”€ data-model.md        # Network topology & resource model (Phase 1 - completed)
β”œβ”€β”€ quickstart.md        # Deployment guide (Phase 1 - completed)
β”œβ”€β”€ checklists/
β”‚   └── requirements.md  # Quality validation checklist (passed)
└── tasks.md             # Task decomposition (Phase 2 - not yet created)
```

### Infrastructure Code Structure

```text
avm-workload/
β”œβ”€β”€ infra/
β”‚   β”œβ”€β”€ main.bicep           # Main deployment template (to be created)
β”‚   β”œβ”€β”€ main.bicepparam      # Deployment parameters (to be created)
β”‚   └── bicepconfig.json     # Bicep analyzer configuration (to be created)
β”œβ”€β”€ specs/
β”‚   └── 001-legacy-vm-workload/  # This feature documentation
β”œβ”€β”€ .specify/
β”‚   β”œβ”€β”€ memory/
β”‚   β”‚   └── constitution.md  # Governance framework
β”‚   β”œβ”€β”€ templates/           # SpecKit templates
β”‚   └── scripts/             # SpecKit automation scripts
└── README.md                # Project overview (to be created)
```

**Structure Decision**: Single-template Infrastructure-as-Code approach using Bicep with Azure Verified Modules. All 20+ resources (VM, VNet, Bastion, Storage, Key Vault, NSGs, NAT Gateway, Log Analytics, Alerts, Private Endpoint, DNS Zone, diagnostic settings, RBAC assignments) defined in one `main.bicep` file with ARM handling dependency ordering automatically. No custom modules needed - all functionality provided by AVM modules.

## Complexity Tracking

**No Constitution Violations** - This section is intentionally empty.

All 6 constitution principles are satisfied with no exceptions required. See Constitution Check section above for detailed compliance status.

---

## Implementation Phases

### Phase 0: Research & Architecture βœ… COMPLETED

**Objective**: Identify required AVM modules, resolve technical unknowns, document architectural decisions.

**Artifacts Created**:
- βœ… [research.md](./research.md) - AVM module inventory with latest versions, alternatives considered, implementation patterns
- βœ… [data-model.md](./data-model.md) - Network topology, resource dependencies, configuration model, security model
- βœ… [quickstart.md](./quickstart.md) - Step-by-step deployment guide with validation commands

**Key Decisions**:
1. **VM Password Generation**: Use Bicep `uniqueString()` function with multiple seeds (resourceGroup().id, deployment().name) - no external scripts needed
2. **Network Addressing**: VNet 10.0.0.0/24 with VM subnet /27, Bastion subnet /26, PE subnet /27
3. **Storage Access**: Private endpoint with private DNS zone integration (privatelink.file.core.windows.net)
4. **Naming Pattern**: `{type}-{purpose}-{uniqueString(6)}` for all resources
5. **Dependency Management**: Single-template approach, let ARM handle resource ordering automatically
6. **Diagnostic Settings**: All resources send logs/metrics to centralized Log Analytics workspace

**Unknowns Resolved**:
- βœ… VNet sizing: 10.0.0.0/24 confirmed sufficient (256 IPs)
- βœ… File share quota: 1TB (1024 GiB)
- βœ… Disk alert threshold: 85%
- βœ… Notification method: Azure Portal only (no Action Groups)
- βœ… VM size: Standard_D2s_v3
- βœ… AVM modules exist for all 11 required Azure resource types

### Phase 1: Infrastructure Code Implementation πŸ”„ IN PROGRESS

**Objective**: Create Bicep templates with AVM module references, parameter file, and configuration.

#### Task 1.1: Create bicepconfig.json ⏳ PENDING

**File**: `infra/bicepconfig.json`

**Purpose**: Configure Bicep analyzer to enforce AVM best practices and warn on outdated module versions.

**Configuration**:
```json
{
  "analyzers": {
    "core": {
      "enabled": true,
      "rules": {
        "use-recent-module-versions": {
          "level": "warning"
        }
      }
    }
  },
  "moduleAliases": {
    "br": {
      "public": {
        "registry": "mcr.microsoft.com",
        "modulePath": "bicep"
      }
    }
  }
}
```

**Validation**: Run `bicep build main.bicep` and verify no analyzer warnings.

#### Task 1.2: Create main.bicep ⏳ PENDING

**File**: `infra/main.bicep`

**Purpose**: Single deployment template referencing 12 AVM modules with proper parameters.

**Structure** (700-900 lines estimated):

1. **Header** (lines 1-30):
  - Metadata: name, description, owner
  - Target scope: `targetScope = 'resourceGroup'`
  - Parameters: vmSize, vmAdminUsername, availabilityZone, fileShareQuotaGiB, logAnalyticsRetentionDays

2. **Variables** (lines 31-80):
  - Random suffix: `var suffix = uniqueString(resourceGroup().id)`
  - Resource names: all following `{type}-{purpose}-${suffix}` pattern
  - VM password: `var vmPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name)}'`
  - Network configuration: subnet CIDR blocks, NSG rules
  - Tags: workload, environment, compliance, managedBy, deploymentDate

3. **Log Analytics Workspace** (lines 81-110):
  ```bicep
  module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.15.0' = {
    name: 'deploy-log-analytics'
    params: {
      name: 'law-legacyvm-${suffix}'
      location: location
      retentionInDays: logAnalyticsRetentionDays
      tags: tags
    }
  }
  ```

4. **Virtual Network** (lines 111-200):
  - Module: `avm/res/network/virtual-network:0.7.2`
  - 3 subnets: VM (10.0.0.0/27), Bastion (10.0.0.64/26), PE (10.0.0.128/27)
  - Diagnostic settings to Log Analytics

5. **Network Security Groups** (lines 201-350):
  - Module: `avm/res/network/network-security-group:0.5.2` (3 instances)
  - NSG 1: VM subnet (deny all inbound, allow internet + VNet outbound)
  - NSG 2: Bastion subnet (standard Azure Bastion rules)
  - NSG 3: PE subnet (allow VM subnet inbound on 445, allow all outbound)
  - Associate each NSG with its subnet
  - Diagnostic settings to Log Analytics

6. **NAT Gateway** (lines 351-380):
  - Module: `avm/res/network/nat-gateway:2.0.1`
  - Public IP auto-created
  - Associate with VM subnet
  - Diagnostic settings to Log Analytics

7. **Azure Bastion** (lines 381-410):
  - Module: `avm/res/network/bastion-host:0.8.2`
  - Depends on VNet and Bastion NSG
  - Public IP auto-created
  - Diagnostic settings to Log Analytics

8. **Key Vault** (lines 411-470):
  - Module: `avm/res/key-vault/vault:0.13.3`
  - SKU: Standard
  - Access model: RBAC
  - Secret: VM admin password (generated variable)
  - RBAC assignment: VM managed identity β†’ Key Vault Secrets User role
  - Diagnostic settings to Log Analytics

9. **Private DNS Zone** (lines 471-500):
  - Module: `avm/res/network/private-dns-zone:0.8.0`
  - Zone name: `privatelink.file.core.windows.net`
  - VNet link to main VNet
  - Depends on VNet

10. **Storage Account** (lines 501-580):
    - Module: `avm/res/storage/storage-account:0.31.0`
    - Kind: StorageV2, SKU: Standard_LRS
    - Public network access: Disabled
    - File share: 1024 GiB quota
    - Diagnostic settings to Log Analytics

11. **Private Endpoint** (lines 581-620):
    - Module: `avm/res/network/private-endpoint:0.11.1`
    - Service: file
    - Subnet: Private Endpoint subnet
    - DNS integration: Private DNS zone
    - Depends on Storage Account, VNet, Private DNS Zone

12. **Virtual Machine** (lines 621-730):
    - Module: `avm/res/compute/virtual-machine:0.21.0`
    - OS: Windows Server 2016
    - Size: Standard_D2s_v3
    - Admin username: parameter
    - Admin password: Key Vault secret reference
    - System-assigned managed identity
    - OS disk: Standard HDD
    - Data disk: 500GB Standard HDD, LUN 0
    - NIC: VM subnet, dynamic private IP, no public IP
    - Availability zone: parameter (1, 2, or 3)
    - Diagnostic settings to Log Analytics
    - Depends on VNet, Key Vault

13. **Metric Alerts** (lines 731-850):
    - Module: `avm/res/insights/metric-alert:0.4.1` (3 instances)
    - Alert 1: VM stopped (CPU < 1% for 15 min, Sev 0)
    - Alert 2: Disk space >85% (OS disk used %, Sev 0)
    - Alert 3: Key Vault access failures (SecretGet failures, Sev 0)
    - No action groups (portal-only notifications)
    - Depends on VM and Key Vault

14. **Outputs** (lines 851-900):
    - VM name and resource ID
    - Key Vault name and resource ID
    - Storage account name and file share name
    - Bastion name
    - Log Analytics workspace ID
    - VNet name and resource ID

**Key Implementation Notes**:
- Use AVM module built-in interfaces for diagnostic settings (NOT direct `Microsoft.Insights/diagnosticSettings` resources)
- Use AVM module built-in interfaces for RBAC role assignments (NOT direct `Microsoft.Authorization/roleAssignments` resources)
- Use AVM module built-in interfaces for locks if needed (NOT direct `Microsoft.Authorization/locks` resources)
- Reference Key Vault secret for VM password using AVM module's secret reference parameter
- ARM will automatically determine deployment order based on dependencies
- All resource names use `uniqueString(resourceGroup().id)` for suffix (6 chars, consistent across all resources)
- Storage account name: no hyphens (Azure requirement), format: `st${replace(suffix, '-', '')}` (max 24 chars)
- VM computer name: max 15 chars, format: `vm-${substring(suffix, 0, 10)}`

#### Task 1.3: Create main.bicepparam ⏳ PENDING

**File**: `infra/main.bicepparam`

**Purpose**: Parameter file for deployment with sensible defaults.

**Content**:
```bicep
using './main.bicep'

// VM Configuration
param vmSize = 'Standard_D2s_v3'
param vmAdminUsername = 'vmadmin'
param availabilityZone = 1

// Storage Configuration
param fileShareQuotaGiB = 1024

// Monitoring Configuration
param logAnalyticsRetentionDays = 30

// Optional overrides (uncomment to customize)
// param vmName = 'vm-custom-name'
// param keyVaultName = 'kv-custom-name'
```

**Validation**: Ensure all parameters match those defined in `main.bicep`.

#### Task 1.4: Create project README.md ⏳ PENDING

**File**: `README.md` (root of repository)

**Purpose**: Project overview with quickstart and links to detailed documentation.

**Sections**:
1. Project Overview
2. Architecture Summary (link to data-model.md)
3. Prerequisites (link to quickstart.md)
4. Quick Deployment (3-step process)
5. Documentation Links (spec.md, plan.md, research.md, quickstart.md)
6. Governance (link to constitution.md)
7. Support and Contributing

### Phase 2: Validation & Testing πŸ”„ NEXT PHASE

**Objective**: Validate Bicep templates and perform what-if analysis before deployment.

#### Task 2.1: Bicep Build Validation

```powershell
cd infra
bicep build main.bicep
# Expected: main.json created, zero warnings
```

**Success Criteria**:
- No compilation errors
- No analyzer warnings
- Output ARM JSON file created successfully

#### Task 2.2: ARM Template Validation

```powershell
az group create --name rg-legacyvm-test --location westus3

az deployment group validate `
  --resource-group rg-legacyvm-test `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose
```

**Success Criteria**:
- Validation passes with `provisioningState: Succeeded`
- No errors related to missing resource providers
- No errors related to invalid parameters

#### Task 2.3: What-If Analysis

```powershell
az deployment group what-if `
  --resource-group rg-legacyvm-test `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose
```

**Success Criteria**:
- All expected resources show as "Create" (green +)
- No unexpected deletions or modifications
- Resource count: 20-25 resources total
- No warnings about deprecated API versions

#### Task 2.4: Code Review Checklist

Manual review of `main.bicep`:

- [ ] All 12 AVM modules referenced with exact versions (no floating versions)
- [ ] All module versions match research.md documentation
- [ ] uniqueString() used consistently for resource name suffixes
- [ ] Storage account name respects 24-char limit and no-hyphen requirement
- [ ] VM computer name respects 15-char limit
- [ ] All subnets have NSG associations
- [ ] NAT Gateway associated with VM subnet only
- [ ] Bastion deployed to AzureBastionSubnet (exact name required)
- [ ] **VM subnet NSG allows inbound RDP (port 3389) from Bastion subnet (10.0.0.64/26) - CRITICAL for bastion connectivity**
- [ ] Private endpoint connects to correct storage service (file)
- [ ] Private DNS zone has VNet link
- [ ] Key Vault secret created with generated password
- [ ] VM references Key Vault secret for password (not plaintext)
- [ ] VM has system-assigned managed identity
- [ ] RBAC assignment: VM identity β†’ Key Vault (Key Vault Secrets User role)
- [ ] All resources have diagnostic settings to Log Analytics
- [ ] All resources have tags applied
- [ ] All resources deploy to westus3 region
- [ ] VM data disk configured: 500GB, Standard HDD, LUN 0
- [ ] File share quota: 1024 GiB
- [ ] 3 metric alerts configured with correct thresholds
- [ ] No action groups on alerts (portal-only requirement)
- [ ] Comments explain each major section

### Phase 3: Deployment πŸ”„ FUTURE PHASE

**Objective**: Deploy infrastructure to Azure and verify all resources operational.

#### Task 3.1: Initial Deployment

**Pre-deployment**:
- Create resource group: `rg-legacyvm-prod`
- Ensure subscription quotas sufficient (VMs, Public IPs, etc.)
- Authenticate to Azure CLI with sufficient permissions

**Deployment Command**:
```powershell
az deployment group create `
  --name "legacyvm-$(Get-Date -Format 'yyyyMMdd-HHmmss')" `
  --resource-group rg-legacyvm-prod `
  --template-file infra/main.bicep `
  --parameters infra/main.bicepparam `
  --verbose
```

**Expected Duration**: 15-20 minutes

**Monitoring**: Track deployment progress in Azure Portal β†’ Resource Groups β†’ rg-legacyvm-prod β†’ Deployments

#### Task 3.2: Post-Deployment Verification

Follow checklist in [quickstart.md](./quickstart.md):

1. **Resource Count Verification**:
  ```powershell
  az resource list --resource-group rg-legacyvm-prod --output table
  # Expected: 20-25 resources
  ```

2. **Bastion Connectivity Test**:
  - Retrieve VM password from Key Vault
  - Connect to VM via Azure Portal Bastion
  - Verify Windows Server 2016 desktop loads

3. **Log Analytics Verification**:
  - Run sample Kusto queries
  - Verify logs appearing for all resources
  - Check for any error logs

4. **Network Connectivity Tests** (from VM):
  - Test internet access via NAT Gateway
  - Verify private endpoint DNS resolution
  - Ping storage account private IP

5. **Alert Verification**:
  - Trigger test alert (Key Vault access failure)
  - Verify alert visible in Azure Portal within 5-10 minutes
  - Confirm alert severity (Sev 0)

#### Task 3.3: Documentation Updates

- Update README.md with actual deployed resource names
- Record deployment timestamp and duration
- Document any deployment issues encountered and resolutions
- Create CHANGELOG.md entry for initial deployment

### Phase 4: Operational Handoff πŸ”„ FUTURE PHASE

**Objective**: Provide operational documentation and ensure supportability.

#### Task 4.1: Operational Runbooks

Create runbooks for common operations:
- VM start/stop procedures
- File share quota increase
- Bastion troubleshooting
- Alert acknowledgment workflow
- Disaster recovery procedure (VM rebuild)

#### Task 4.2: Cost Monitoring Setup

Document estimated monthly costs:
- VM (Standard_D2s_v3): ~$70/month
- Storage (1TB + disks): ~$50/month
- Bastion: ~$140/month
- Other services: ~$10/month
- **Total**: ~$270/month (westus3 region)

Set up Azure Cost Management alerts:
- Budget: $300/month
- Alert threshold: 80% ($240)

#### Task 4.3: Security Review

Complete post-deployment security checklist:
- [ ] All resources have diagnostic logging enabled
- [ ] VM has no public IP address
- [ ] Storage account public access disabled
- [ ] Key Vault access restricted to VM managed identity
- [ ] NSG rules follow least-privilege principle
- [ ] Azure Security Center recommendations reviewed
- [ ] No high-severity vulnerabilities identified

#### Task 4.4: Compliance Documentation

Document compliance controls met:
- Infrastructure-as-Code: All resources in version control
- Audit logging: All activity logged to Log Analytics
- Secret management: Passwords in Key Vault, not plaintext
- Network isolation: Private endpoints, no public exposure
- Change management: Deployments require validation gate

### Phase 5: Future Enhancements πŸ”„ OUT OF SCOPE (DOCUMENTED FOR REFERENCE)

Items explicitly out of scope for initial deployment but may be added later:

1. **File Share VM Integration**:
  - Map file share as network drive in VM
  - Configure persistent drive mapping via Group Policy or startup script
  - Document in operational runbooks

2. **Advanced Monitoring**:
  - Custom Log Analytics queries and workbooks
  - Action Groups for email/SMS notifications
  - Integration with external monitoring systems

3. **Backup Configuration**:
  - Azure Backup for VM
  - Azure Files snapshot/backup policies
  - Backup retention policy aligned with compliance requirements

4. **High Availability** (if requirements change):
  - Availability Set or multiple VMs across zones
  - Load Balancer for multi-VM scenarios
  - Azure Site Recovery for disaster recovery

5. **Security Enhancements**:
  - Just-In-Time VM Access
  - Azure Policy assignments
  - Microsoft Defender for Cloud integration
  - Network Watcher flow logs

---

## Risk Assessment & Mitigation

### Technical Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| AVM module breaking changes | Low | Medium | Pin exact module versions, monitor AVM changelogs |
| Bastion deployment timeout | Medium | Low | Allow 20-30 min deployment window, retry if needed |
| Key Vault access issues | Low | High | Thorough RBAC testing, fallback manual password retrieval |
| Private endpoint DNS resolution failures | Low | Medium | Verify Private DNS Zone VNet link, wait for propagation |
| VM extension failures (Windows diagnostics) | Medium | Low | Monitor deployment, acceptable to complete manually |
| Storage account naming conflicts | Low | Low | uniqueString() ensures uniqueness per resource group |

### Operational Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Lost VM password | Low | High | Key Vault provides secure retrieval, document procedure |
| Excessive costs (Bastion always-on) | Medium | Medium | Document monthly costs, set budget alerts at 80% |
| Alert fatigue (too many alerts) | Low | Medium | Start with 3 critical alerts, expand based on operations feedback |
| Insufficient disk space (500GB data disk) | Medium | Low | Alert at 85%, expansion procedure documented |
| Network connectivity issues | Low | High | NAT Gateway provides reliable outbound, private endpoint for inbound |
| Manual Portal changes breaking IaC | Medium | High | Enforce constitution principle I, document prohibition clearly |

### Compliance Risks

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| Configuration drift from manual changes | Medium | High | Constitution principle I (IaC-First) prohibits manual changes |
| Audit log gaps | Low | High | All resources send logs to Log Analytics, monitor for gaps |
| Secret exposure (VM password) | Low | Critical | Password generated in Bicep variable, stored only in Key Vault |
| Unauthorized access attempts | Low | High | No public IPs, Bastion-only access, NSG least-privilege rules |
| Non-compliance with retention policies | Low | Medium | Log Analytics retention set to 30+ days, configurable parameter |

---

## Success Criteria

### Deployment Success

- [x] All Bicep templates compile without warnings
- [ ] ARM validation passes successfully
- [ ] What-if analysis shows expected resource creation only
- [ ] Deployment completes in <30 minutes
- [ ] All 20-25 resources created successfully
- [ ] No failed resources or partial deployments

### Functional Success

- [ ] VM accessible via Azure Bastion RDP
- [ ] VM admin password retrievable from Key Vault
- [ ] VM has internet connectivity via NAT Gateway
- [ ] File share accessible from VM via private endpoint
- [ ] Storage account private endpoint resolves to internal IP (10.0.0.128/27 range)
- [ ] All resources logging to Log Analytics
- [ ] 3 metric alerts visible in Azure Portal
- [ ] Test alert fires successfully within 10 minutes

### Compliance Success

- [x] All 6 constitution principles satisfied (see Constitution Check)
- [ ] All resources deployed to westus3 region
- [ ] All resources follow naming convention: {type}-{purpose}-{random}
- [ ] No manual Portal configurations required post-deployment
- [ ] Deployment process documented in quickstart.md
- [ ] All secrets stored in Key Vault, none in version control

### Operational Success

- [ ] Quickstart guide executed successfully by independent tester
- [ ] VM operational for 24 hours with no errors
- [ ] Monitoring dashboards show healthy resource state
- [ ] No high-severity Security Center alerts
- [ ] Total monthly cost projection: $250-$300 USD
- [ ] Documentation sufficient for operational handoff

---

## Appendices

### A. Resource Naming Reference

| Resource Type | Name Pattern | Example |
|---------------|-------------|---------|
| Virtual Machine | `vm-legacyvm-{random}` | `vm-legacyvm-k7m3p` |
| Virtual Network | `vnet-legacyvm-{random}` | `vnet-legacyvm-k7m3p` |
| Subnets | `snet-{purpose}-legacyvm-{random}` | `snet-vm-legacyvm-k7m3p` |
| Network Security Group | `nsg-{purpose}-legacyvm-{random}` | `nsg-vm-legacyvm-k7m3p` |
| NAT Gateway | `nat-legacyvm-{random}` | `nat-legacyvm-k7m3p` |
| Azure Bastion | `bas-legacyvm-{random}` | `bas-legacyvm-k7m3p` |
| Key Vault | `kv-legacyvm-{random}` | `kv-legacyvm-k7m3p` |
| Storage Account | `st{random-no-hyphens}` | `stk7m3p2a` |
| Log Analytics Workspace | `law-legacyvm-{random}` | `law-legacyvm-k7m3p` |
| Metric Alert | `alert-{purpose}-legacyvm-{random}` | `alert-disk-space-legacyvm-k7m3p` |
| Private Endpoint | `pe-{service}-legacyvm-{random}` | `pe-file-legacyvm-k7m3p` |
| Private DNS Zone | `privatelink.{service}.core.windows.net` | `privatelink.file.core.windows.net` |

**Random Suffix**: Generated using `uniqueString(resourceGroup().id)` - produces 13 characters, take first 6: `k7m3p2`

### B. Network Address Allocation

| Component | CIDR | Usable IPs | Purpose |
|-----------|------|------------|---------|
| VNet | 10.0.0.0/24 | 256 | Overall network |
| VM Subnet | 10.0.0.0/27 | 27 usable | Virtual machine NICs |
| Bastion Subnet | 10.0.0.64/26 | 59 usable | Azure Bastion (requires /26) |
| Private Endpoint Subnet | 10.0.0.128/27 | 27 usable | Storage private endpoint |
| Reserved | 10.0.0.160/27 | 27 usable | Future expansion |
| Reserved | 10.0.0.192/26 | 59 usable | Future expansion |

### C. Required Azure Permissions

**Subscription-Level**:
- `Contributor` role OR specific resource provider permissions
- `User Access Administrator` role (for RBAC assignments to Key Vault)

**Resource Providers** (must be registered):
- `Microsoft.Compute`
- `Microsoft.Network`
- `Microsoft.Storage`
- `Microsoft.KeyVault`
- `Microsoft.Insights`
- `Microsoft.OperationalInsights`

### D. Tool Version Requirements

| Tool | Minimum Version | Recommended Version | Check Command |
|------|----------------|---------------------|---------------|
| Azure CLI | 2.65.0 | Latest | `az --version` |
| Bicep CLI | 0.33.0 | Latest | `az bicep version` |
| PowerShell | 7.4 | Latest | `$PSVersionTable.PSVersion` |

### E. Related Documentation

- **Feature Specification**: [spec.md](./spec.md) - Detailed requirements and user stories
- **Module Research**: [research.md](./research.md) - AVM module analysis and alternatives
- **Data Model**: [data-model.md](./data-model.md) - Network topology and resource relationships
- **Deployment Guide**: [quickstart.md](./quickstart.md) - Step-by-step deployment instructions
- **Quality Checklist**: [checklists/requirements.md](./checklists/requirements.md) - Validation results
- **Governance**: [../../.specify/memory/constitution.md](../../.specify/memory/constitution.md) - Project constitution

### F. Change Log

| Date | Version | Author | Changes |
|------|---------|--------|---------|
| 2026-01-27 | 1.0.0 | SpecKit | Initial implementation plan created |

---

**Plan Status**: Phase 1 (Infrastructure Code Implementation) - Ready for bicepconfig.json and main.bicep creation

**Next Command**: `/speckit.tasks` to decompose Phase 1 tasks into granular implementation steps
<!-- markdownlint-disable -->
# Data Model: Legacy VM Workload Infrastructure

**Date**: 2026-01-27
**Feature**: [spec.md](../spec.md)
**Purpose**: Document network topology, resource relationships, and configuration data model

## Network Topology

### Virtual Network Structure

```
VNet: 10.0.0.0/24 (vnet-legacyvm-{random})
β”‚
β”œβ”€β”€ VM Subnet: 10.0.0.0/27 (snet-vm-legacyvm-{random})
β”‚   β”œβ”€β”€ NAT Gateway attached
β”‚   β”œβ”€β”€ NSG attached (nsg-vm-legacyvm-{random})
β”‚   β”œβ”€β”€ VM Network Interface
β”‚   └── Hosts: Virtual Machine
β”‚
β”œβ”€β”€ Bastion Subnet: 10.0.0.64/26 (AzureBastionSubnet - required name)
β”‚   β”œβ”€β”€ NSG attached (nsg-bastion-legacyvm-{random})
β”‚   └── Hosts: Azure Bastion
β”‚
└── Private Endpoint Subnet: 10.0.0.128/27 (snet-pe-legacyvm-{random})
    β”œβ”€β”€ NSG attached (nsg-pe-legacyvm-{random})
    └── Hosts: Storage Account Private Endpoint
```

### Address Space Allocation

| Resource | CIDR | Usable IPs | Purpose |
|----------|------|------------|---------|
| VNet | 10.0.0.0/24 | 256 | Overall network |
| VM Subnet | 10.0.0.0/27 | 32 (27 usable) | Virtual machine network interfaces |
| Bastion Subnet | 10.0.0.64/26 | 64 (59 usable) | Azure Bastion (requires /26 minimum) |
| Private Endpoint Subnet | 10.0.0.128/27 | 32 (27 usable) | Storage account private endpoints |
| Reserved | 10.0.0.160/27 | 32 | Future expansion |
| Reserved | 10.0.0.192/26 | 64 | Future expansion |

## Resource Dependency Graph

```
Resource Group
β”‚
β”œβ”€β”€ Log Analytics Workspace
β”‚   └── (Used by all diagnostic settings)
β”‚
β”œβ”€β”€ Virtual Network
β”‚   β”œβ”€β”€ Depends on: None
β”‚   └── Used by: Bastion, VM NIC, Private Endpoint
β”‚
β”œβ”€β”€ Network Security Groups (3)
β”‚   β”œβ”€β”€ nsg-vm-legacyvm-{random}
β”‚   β”œβ”€β”€ nsg-bastion-legacyvm-{random}
β”‚   └── nsg-pe-legacyvm-{random}
β”‚   β”œβ”€β”€ Depends on: VNet (for subnet association)
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ NAT Gateway
β”‚   β”œβ”€β”€ Public IP (auto-created)
β”‚   β”œβ”€β”€ Depends on: None
β”‚   β”œβ”€β”€ Associated with: VM Subnet
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ Azure Bastion
β”‚   β”œβ”€β”€ Public IP (auto-created)
β”‚   β”œβ”€β”€ Depends on: VNet (Bastion subnet)
β”‚   β”œβ”€β”€ Depends on: NSG (bastion subnet)
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ Key Vault
β”‚   β”œβ”€β”€ Depends on: None (deployed early)
β”‚   β”œβ”€β”€ Secret: VM admin password (generated)
β”‚   β”œβ”€β”€ RBAC: VM managed identity (Key Vault Secrets User)
β”‚   └── Diagnostic settings β†’ Log Analytics
β”‚
β”œβ”€β”€ Private DNS Zone
β”‚   β”œβ”€β”€ Name: privatelink.file.core.windows.net
β”‚   β”œβ”€β”€ VNet Link: Main VNet
β”‚   └── Depends on: VNet
β”‚
β”œβ”€β”€ Storage Account
β”‚   β”œβ”€β”€ File Share (1024 GiB)
β”‚   β”œβ”€β”€ Depends on: None
β”‚   β”œβ”€β”€ Public access: Disabled
β”‚   β”œβ”€β”€ Diagnostic settings β†’ Log Analytics
β”‚   └── Private Endpoint
β”‚       β”œβ”€β”€ Depends on: Storage Account, VNet, Private DNS Zone
β”‚       β”œβ”€β”€ Subnet: Private Endpoint Subnet
β”‚       └── DNS integration: Private DNS Zone
β”‚
└── Virtual Machine
    β”œβ”€β”€ Depends on: VNet, Key Vault (for password)
    β”œβ”€β”€ Managed Identity: System-assigned
    β”œβ”€β”€ OS Disk: Standard HDD
    β”œβ”€β”€ Data Disk: 500GB Standard HDD
    β”œβ”€β”€ Network Interface
    β”‚   β”œβ”€β”€ Depends on: VM Subnet
    β”‚   └── No Public IP
    β”œβ”€β”€ Password: Retrieved from Key Vault secret
    └── Diagnostic settings β†’ Log Analytics

β”œβ”€β”€ Azure Monitor Alerts (3)
    β”œβ”€β”€ VM Stopped Alert
    β”‚   └── Depends on: VM
    β”œβ”€β”€ Disk Space Alert
    β”‚   └── Depends on: VM
    └── Key Vault Access Failures Alert
        └── Depends on: Key Vault
```

## Resource Configuration Model

### Virtual Machine
```yaml
Name Pattern: vm-legacyvm-{random}
Computer Name: vm-{random} (≀15 chars total)
Configuration:
  Size: Standard_D2s_v3
  OS: Windows Server 2016
  OS Disk:
    Type: Standard_LRS (HDD performance tier)
    Size: Default (127 GB or OS default)
  Data Disks:
    - Name: datadisk-01
      Size: 500 GB
      Type: Standard_LRS (HDD performance tier)
      LUN: 0
  Admin:
    Username: vmadmin
    Password: {From Key Vault secret}
  Identity:
    Type: SystemAssigned
  Zone: {Parameter: 1, 2, or 3}
  Network:
    NIC:
      Subnet: VM Subnet
      Public IP: None
      Private IP: Dynamic
  Diagnostics:
    Boot Diagnostics: Enabled (Managed)
    Guest Diagnostics: Windows (via Log Analytics agent)
```

### Key Vault
```yaml
Name Pattern: kv-legacyvm-{random}
Configuration:
  SKU: Standard
  Access Model: RBAC (Azure role-based access control)
  Public Network Access: Enabled (simplified for legacy workload)
  Soft Delete: Enabled (90 days)
  Purge Protection: Disabled (not required for legacy workload)
  Secrets:
    - Name: {Parameter: vmAdminPasswordSecretName}
      Value: {Generated: uniqueString-based password}
      Content Type: text/plain
  RBAC Assignments:
    - Principal: VM Managed Identity
      Role: Key Vault Secrets User
      Scope: Key Vault
  Diagnostics:
    Logs: All categories
    Metrics: All metrics
    Destination: Log Analytics
```

### Storage Account
```yaml
Name Pattern: st{random-no-hyphens} (≀24 chars)
Configuration:
  Kind: StorageV2
  SKU: Standard_LRS (HDD-based)
  Access Tier: Hot
  Public Network Access: Disabled
  Minimum TLS: 1.2
  File Services:
    Shares:
      - Name: fileshare
        Quota: 1024 GiB
        Access Tier: TransactionOptimized
  Private Endpoints:
    - Service: file
      Subnet: Private Endpoint Subnet
      DNS Integration: privatelink.file.core.windows.net
  Diagnostics:
    Logs: All categories (StorageRead, StorageWrite, StorageDelete)
    Metrics: All metrics
    Destination: Log Analytics
```

### Network Security Groups

#### VM Subnet NSG
```yaml
Name: nsg-vm-legacyvm-{random}
Security Rules:
  Inbound:
    - Name: DenyAllInbound
      Priority: 4096
      Direction: Inbound
      Access: Deny
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
  Outbound:
    - Name: AllowInternetOutbound
      Priority: 100
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: Internet
      SourcePort: *
      DestinationPort: *
    - Name: AllowVnetOutbound
      Priority: 200
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: *
    - Name: DenyAllOutbound
      Priority: 4096
      Direction: Outbound
      Access: Deny
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
```

#### Bastion Subnet NSG
```yaml
Name: nsg-bastion-legacyvm-{random}
Security Rules:
  # Standard Azure Bastion required rules
  Inbound:
    - Name: AllowHttpsInbound
      Priority: 100
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: Internet
      Destination: *
      SourcePort: *
      DestinationPort: 443
    - Name: AllowGatewayManagerInbound
      Priority: 110
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: GatewayManager
      Destination: *
      SourcePort: *
      DestinationPort: 443
    - Name: AllowAzureLoadBalancerInbound
      Priority: 120
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: AzureLoadBalancer
      Destination: *
      SourcePort: *
      DestinationPort: 443
    - Name: AllowBastionHostCommunication
      Priority: 130
      Direction: Inbound
      Access: Allow
      Protocol: *
      Source: VirtualNetwork
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: 8080,5701
  Outbound:
    - Name: AllowSshRdpOutbound
      Priority: 100
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: 22,3389
    - Name: AllowAzureCloudOutbound
      Priority: 110
      Direction: Outbound
      Access: Allow
      Protocol: Tcp
      Source: *
      Destination: AzureCloud
      SourcePort: *
      DestinationPort: 443
    - Name: AllowBastionCommunication
      Priority: 120
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: VirtualNetwork
      Destination: VirtualNetwork
      SourcePort: *
      DestinationPort: 8080,5701
    - Name: AllowGetSessionInformation
      Priority: 130
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: Internet
      SourcePort: *
      DestinationPort: 80
```

#### Private Endpoint Subnet NSG
```yaml
Name: nsg-pe-legacyvm-{random}
Security Rules:
  Inbound:
    - Name: AllowVMSubnetInbound
      Priority: 100
      Direction: Inbound
      Access: Allow
      Protocol: Tcp
      Source: 10.0.0.0/27
      Destination: *
      SourcePort: *
      DestinationPort: 445
    - Name: DenyAllInbound
      Priority: 4096
      Direction: Inbound
      Access: Deny
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
  Outbound:
    - Name: AllowAllOutbound
      Priority: 100
      Direction: Outbound
      Access: Allow
      Protocol: *
      Source: *
      Destination: *
      SourcePort: *
      DestinationPort: *
```

### Azure Monitor Alerts

#### Alert 1: VM Stopped/Deallocated
```yaml
Name: alert-vm-stopped-legacyvm-{random}
Configuration:
  Type: Metric
  Target: Virtual Machine
  Metric:
    Namespace: Microsoft.Compute/virtualMachines
    Name: Percentage CPU
  Condition:
    Operator: LessThan
    Threshold: 1
    Aggregation: Average
    Window: 15 minutes
  Severity: Critical (Sev 0)
  Auto-Mitigate: false
  Description: "Critical: VM appears to be stopped or deallocated"
```

#### Alert 2: Disk Space Exceeded
```yaml
Name: alert-disk-space-legacyvm-{random}
Configuration:
  Type: Metric
  Target: Virtual Machine
  Metric:
    Namespace: Microsoft.Compute/virtualMachines
    Name: OS Disk Used Percentage
  Condition:
    Operator: GreaterThan
    Threshold: 85
    Aggregation: Average
    Window: 5 minutes
  Severity: Critical (Sev 0)
  Auto-Mitigate: false
  Description: "Critical: Disk space exceeded 85% threshold"
```

#### Alert 3: Key Vault Access Failures
```yaml
Name: alert-kv-access-fail-legacyvm-{random}
Configuration:
  Type: Metric
  Target: Key Vault
  Metric:
    Namespace: Microsoft.KeyVault/vaults
    Name: ServiceApiHit
  Filter:
    Dimension: ActivityName
    Values: SecretGet
    Result: Failed
  Condition:
    Operator: GreaterThan
    Threshold: 0
    Aggregation: Count
    Window: 5 minutes
  Severity: Critical (Sev 0)
  Auto-Mitigate: false
  Description: "Critical: Key Vault secret access failures detected"
```

## Deployment Sequence

Based on ARM dependency analysis, resources will deploy in this approximate order:

1. **Phase 1: Foundation** (Parallel)
  - Log Analytics Workspace
  - Virtual Network (with subnets)
  - Network Security Groups

2. **Phase 2: Network & Security** (Depends on Phase 1)
  - NAT Gateway (associates with VM subnet)
  - Azure Bastion (requires subnet and NSG)
  - Private DNS Zone (requires VNet)
  - Key Vault (generates and stores password)

3. **Phase 3: Storage** (Depends on Phase 2)
  - Storage Account (with file share)
  - Private Endpoint (requires storage account, VNet, DNS zone)

4. **Phase 4: Compute** (Depends on Phases 1-3)
  - Virtual Machine (requires VNet, Key Vault secret, zone assignment)

5. **Phase 5: Monitoring** (Depends on Phase 4)
  - Azure Monitor Alerts (require VM and Key Vault to be deployed)

## Parameter Data Model

```yaml
# Required Parameters
parameters:
  resourceGroupName: string
    description: Name of the resource group for deployment
    example: rg-legacyvm-prod

  location: string
    description: Azure region for deployment
    default: westus3
    validation: Must be valid Azure region

  vmSize: string
    description: Virtual machine size
    default: Standard_D2s_v3
    validation: Must support Windows Server 2016

  vmAdminUsername: string
    description: VM administrator username
    default: vmadmin
    minLength: 1
    maxLength: 20

  vmAdminPasswordSecretName: string
    description: Name of Key Vault secret for VM admin password
    default: vm-admin-password
    minLength: 1
    maxLength: 127

  availabilityZone: int
    description: Availability zone for zone-capable resources
    allowed: [1, 2, 3]
    default: 1

  fileShareQuotaGiB: int
    description: File share quota in GiB
    default: 1024
    minValue: 100
    maxValue: 102400

  logAnalyticsRetentionDays: int
    description: Log Analytics data retention in days
    default: 30
    minValue: 30
    maxValue: 730

# Generated Values (not parameters)
variables:
  randomSuffix: uniqueString(resourceGroup().id)
  vmPassword: P@ssw0rd!{uniqueString(resourceGroup().id, deployment().name)}

  # Resource Names
  vnetName: vnet-legacyvm-{randomSuffix}
  vmName: vm-legacyvm-{randomSuffix}
  storageAccountName: st{replace(randomSuffix, '-', '')}
  keyVaultName: kv-legacyvm-{randomSuffix}
  logAnalyticsName: law-legacyvm-{randomSuffix}
```

## Tags Model

All resources will be tagged with:

```yaml
tags:
  workload: legacy-vm
  environment: production
  compliance: legacy-retention
  managedBy: bicep-avm
  deploymentDate: {deployment().timestamp}
```

## Security Model

### RBAC Assignments

| Principal | Role | Scope | Purpose |
|-----------|------|-------|---------|
| VM Managed Identity | Key Vault Secrets User | Key Vault | Read VM admin password |
| VM Managed Identity | Storage Blob Data Contributor | Storage Account | Access file share (future) |

### Network Security

| Source | Destination | Protocol/Port | Action | Purpose |
|--------|-------------|---------------|--------|---------|
| Internet | Bastion (443) | TCP/443 | Allow | Admin RDP access |
| Bastion | VM (3389) | TCP/3389 | Allow | RDP to VM |
| VM | Internet | Any | Allow | Outbound via NAT Gateway |
| VM | Storage PE (445) | TCP/445 | Allow | File share access |
| Any | VM | Any | Deny | No direct access to VM |

## Monitoring Data Model

### Diagnostic Settings Targets

All resources with diagnostic settings send to:
- **Primary**: Log Analytics Workspace
- **Categories**: All available log categories
- **Metrics**: All available metrics

### Alert Notification Model

- **Channel**: Azure Portal only
- **No Action Groups**: Alerts visible in portal alerts blade
- **Severity**: All set to Critical (Sev 0)
- **Auto-Mitigation**: Disabled (require manual acknowledgment)
<!-- markdownlint-disable -->
# Research: Legacy VM Workload AVM Modules

**Date**: 2026-01-27
**Feature**: [spec.md](../spec.md)
**Purpose**: Research and document AVM module selections, versions, and configuration approaches

## AVM Module Inventory

### Primary Infrastructure Modules

#### 1. Virtual Network
- **Module**: `avm/res/network/virtual-network`
- **Latest Version**: 0.7.2
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/virtual-network/0.7.2/avm/res/network/virtual-network/README.md
- **Decision**: Use this module for VNet and subnet deployment
- **Rationale**: Official AVM module with built-in support for subnets, NSG assignments, NAT gateway association, and diagnostic settings
- **Key Parameters Needed**:
  - Address space: 10.0.0.0/24
  - Subnets: VM (10.0.0.0/27), Bastion (10.0.0.64/26), Private endpoint (10.0.0.128/27)
  - NSG associations per subnet
  - NAT gateway assignment to VM subnet

#### 2. Virtual Machine
- **Module**: `avm/res/compute/virtual-machine`
- **Latest Version**: 0.21.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/compute/virtual-machine/0.21.0/avm/res/compute/virtual-machine/README.md
- **Decision**: Use this module for VM deployment
- **Rationale**: Comprehensive AVM module with built-in support for managed disks, managed identity, diagnostic settings, and guest configuration
- **Key Parameters Needed**:
  - VM size: Standard_D2s_v3
  - OS: Windows Server 2016
  - Computer name: ≀15 characters
  - Admin username: vmadmin
  - Admin password: Reference to Key Vault secret
  - Managed identity: System-assigned
  - Data disks: 500GB HDD
  - Availability zone: 1-3 (parameter-driven)
  - No public IP

#### 3. Azure Bastion
- **Module**: `avm/res/network/bastion-host`
- **Latest Version**: 0.8.2
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/bastion-host/0.8.2/avm/res/network/bastion-host/README.md
- **Decision**: Use this module for bastion deployment
- **Rationale**: AVM module with built-in diagnostic settings and public IP creation
- **Key Parameters Needed**:
  - Subnet: Bastion subnet (10.0.0.64/26)
  - SKU: Basic (cost-effective for legacy workload)
  - Diagnostic settings to Log Analytics

#### 4. Storage Account
- **Module**: `avm/res/storage/storage-account`
- **Latest Version**: 0.31.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/storage/storage-account/0.31.0/avm/res/storage/storage-account/README.md
- **Decision**: Use this module for storage account and file share
- **Rationale**: Comprehensive AVM module with built-in file share, private endpoint, diagnostic settings, and network rules support
- **Key Parameters Needed**:
  - SKU: Standard_LRS (HDD-based)
  - File share quota: 1024 GiB
  - Private endpoint enabled
  - Public network access disabled
  - Diagnostic settings to Log Analytics

#### 5. NAT Gateway
- **Module**: `avm/res/network/nat-gateway`
- **Latest Version**: 2.0.1
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/nat-gateway/2.0.1/avm/res/network/nat-gateway/README.md
- **Decision**: Use this module for NAT gateway
- **Rationale**: AVM module with public IP creation and zone support
- **Key Parameters Needed**:
  - Zone: parameter-driven (1-3)
  - Public IP: Auto-created by module

#### 6. Network Security Group
- **Module**: `avm/res/network/network-security-group`
- **Latest Version**: 0.5.2
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/network-security-group/0.5.2/avm/res/network/network-security-group/README.md
- **Decision**: Use this module for all three NSGs (VM subnet, bastion subnet, private endpoint subnet)
- **Rationale**: AVM module with built-in diagnostic settings and security rule definitions
- **Key Parameters Needed**:
  - VM subnet NSG: Allow outbound to internet via NAT gateway, deny other traffic
  - Bastion subnet NSG: Standard bastion rules (inbound 443, outbound to VM subnet)
  - Private endpoint subnet NSG: Allow traffic from VM subnet only

#### 7. Key Vault
- **Module**: `avm/res/key-vault/vault`
- **Latest Version**: 0.13.3
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/key-vault/vault/0.13.3/avm/res/key-vault/vault/README.md
- **Decision**: Use this module for Key Vault and secret storage
- **Rationale**: AVM module with built-in secret creation using `secrets` parameter array, RBAC support, diagnostic settings, and network rules
- **Key Features**:
  - Supports `secrets` parameter for creating secrets at deployment time
  - Can generate password using `uniqueString()` and store in secret
  - Built-in RBAC assignments
  - Private endpoint support (optional for this scenario)
  - Diagnostic settings interface
- **Password Generation Approach**:
  - Use Bicep `uniqueString()` function to generate complex password
  - Combine multiple seed values for randomness
  - Store in Key Vault secret via module's `secrets` parameter
  - Reference secret in VM module

#### 8. Log Analytics Workspace
- **Module**: `avm/res/operational-insights/workspace`
- **Latest Version**: 0.15.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/operational-insights/workspace/0.15.0/avm/res/operational-insights/workspace/README.md
- **Decision**: Use this module for Log Analytics
- **Rationale**: AVM module with retention configuration and solution deployment support
- **Key Parameters Needed**:
  - Retention days: 30 (default assumption)
  - SKU: PerGB2018

#### 9. Private Endpoint
- **Module**: `avm/res/network/private-endpoint`
- **Latest Version**: 0.11.1
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/private-endpoint/0.11.1/avm/res/network/private-endpoint/README.md
- **Decision**: Use this module for storage account file share private endpoint
- **Rationale**: AVM module with built-in private DNS zone group configuration
- **Key Parameters Needed**:
  - Service connection: Storage account file service
  - Subnet: Private endpoint subnet
  - Private DNS zone: privatelink.file.core.windows.net (manual creation)

#### 10. Azure Monitor Alerts
- **Module**: `avm/res/insights/metric-alert`
- **Latest Version**: 0.4.1
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/insights/metric-alert/0.4.1/avm/res/insights/metric-alert/README.md
- **Decision**: Use this module for all three critical alerts
- **Rationale**: AVM module supporting metric-based alerts for VM and Key Vault
- **Alerts to Create**:
  1. VM stopped/deallocated
  2. Disk space > 85%
  3. Key Vault access failures
- **Note**: Portal-only notifications (no action groups needed for this scenario)

### Supporting Modules

#### 11. Managed Disk
- **Included in VM Module**: The Virtual Machine module handles data disk creation inline
- **No separate module needed**: Data disks are specified as parameters to the VM module

#### 12. Private DNS Zone
- **Module**: `avm/res/network/private-dns-zone`
- **Latest Version**: 0.8.0
- **Documentation**: https://github.com/Azure/bicep-registry-modules/tree/avm/res/network/private-dns-zone/0.8.0/avm/res/network/private-dns-zone/README.md
- **Decision**: Use this module for private DNS zone for file share private endpoint
- **Rationale**: Required for DNS resolution of storage account file share through private endpoint
- **Key Parameters Needed**:
  - Zone name: privatelink.file.core.windows.net
  - VNet link to main VNet

## Alternative Approaches Considered

### Alternative 1: Direct Resource Declarations
- **Approach**: Use direct Bicep resource declarations instead of AVM modules
- **Rejected**: Violates constitution principle II (AVM-Only Modules)
- **Trade-offs**: Would provide more control but lose benefits of tested, maintained, secure-by-default configurations

### Alternative 2: Pattern Module for VM Workloads
- **Approach**: Search for existing AVM pattern module combining VM, networking, and storage
- **Evaluated**: No suitable pattern module exists for this specific legacy VM scenario
- **Decision**: Compose solution from resource modules per constitution

### Alternative 3: Deployment Scripts for Password Generation
- **Approach**: Use Azure Deployment Scripts to generate and store VM password
- **Rejected**: User requirement specifies using uniqueString() and avoiding external helper scripts
- **Decision**: Generate password inline using Bicep uniqueString() function and store via Key Vault module's secrets parameter

### Alternative 4: Azure Backup Integration
- **Approach**: Include Azure Backup configuration for VM
- **Rejected**: Explicitly out of scope per specification
- **Note**: Can be added later if requirements change

## Bicep Language Features Required

### Password Generation Pattern
```bicep
// Generate complex password using uniqueString with multiple seeds
var generatedPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name, utcNow('u'))}'
```

### Resource Dependency Management
- Let ARM manage dependencies automatically
- Explicit `dependsOn` only when implicit dependency isn't detected
- Use resource symbolic names for references

### Parameter Validation
- Use decorators: `@minLength()`, `@maxLength()`, `@allowed()`
- Validate VM computer name length (≀15 chars)
- Validate storage account name (≀24 chars, lowercase, alphanumeric)

## Key Configuration Decisions

### Resource Naming
- **Pattern**: {resourceType}-{purpose}-{randomSuffix}
- **Random Suffix**: Use uniqueString() with 6 characters
- **Examples**:
  - VNet: `vnet-legacyvm-${uniqueString(resourceGroup().id)}`
  - VM: `vm-legacyvm-${uniqueString(resourceGroup().id)}`
  - Storage: `st${replace(uniqueString(resourceGroup().id), '-', '')}` (no hyphens, ≀24 chars)
  - Key Vault: `kv-legacyvm-${uniqueString(resourceGroup().id)}`

### Network Security
- **VM Subnet NSG**: Allow outbound internet (via NAT), deny all inbound except from bastion
- **Bastion Subnet NSG**: Follow Azure Bastion NSG requirements
- **Private Endpoint Subnet NSG**: Allow inbound from VM subnet on port 445 (SMB)

### Diagnostic Settings
- **Target**: Log Analytics Workspace (centralized)
- **Resources to Monitor**: VM, Key Vault, Storage Account, NSGs, Bastion
- **Log Categories**: All available categories
- **Metrics**: All available metrics

### Availability Zones
- **VM**: Deploy to zone specified by parameter (1, 2, or 3)
- **NAT Gateway**: Deploy to same zone as VM
- **Managed Disks**: Automatically zone-aligned with VM

## Implementation Notes

### Single Template Approach
- All resources in main.bicep
- No nested modules or separate Bicep files
- ARM dependency management handles deployment order

### Parameter Management
- All configurable values in main.bicepparam
- Rich comments explaining each parameter
- Default values where appropriate
- No hardcoded values in template

### Bicep CLI Version
- **Minimum**: Latest stable version (0.33.0 or higher at time of writing)
- **Recommendation**: Always use latest for newest AVM module support
- **Verification**: Run `bicep --version` before deployment

### Module Version Pinning
- **Required**: Always pin to specific versions (never 'latest' tag)
- **Format**: `br/public:avm/res/network/virtual-network:0.7.2`
- **Maintenance**: Update versions explicitly when needed

## Open Questions Resolved

1. **How to generate VM password without external scripts?**
  - **Resolution**: Use Bicep `uniqueString()` function with multiple seeds
  - **Implementation**: Store generated password in Key Vault using module's `secrets` parameter

2. **How to connect file share to VM?**
  - **Resolution**: Out of scope for initial deployment per user guidance
  - **Future**: Will require VM extension or post-deployment script

3. **Should we use private endpoint for Key Vault?**
  - **Resolution**: No, not required for this legacy workload
  - **Justification**: Adds complexity without clear benefit for single VM scenario

4. **What alert notification channels?**
  - **Resolution**: Portal notifications only (clarified during specification)
  - **Implementation**: Create metric alerts without action groups

5. **Module version for optimal features?**
  - **Resolution**: Always use latest stable version listed in AVM metadata
  - **Verification**: Confirmed all required features available in latest versions

## Next Steps

1. Create data-model.md with network topology and resource relationships
2. Write deployment quickstart guide
3. Fill implementation plan template
4. Create bicepconfig.json with module version analyzer
<!-- markdownlint-disable -->
# Quickstart: Deploy Legacy VM Workload

**Date**: 2026-01-27
**Feature**: [spec.md](../spec.md)
**Purpose**: Step-by-step deployment guide with validation and troubleshooting

## Prerequisites

### Required Tools

1. **Azure CLI** (v2.65.0 or later)
  ```powershell
  # Check version
  az --version

  # Install/upgrade if needed
  # Windows: Download from https://aka.ms/installazurecliwindows
  # Or use winget
  winget install -e --id Microsoft.AzureCLI
  ```

2. **Bicep CLI** (v0.33.0 or later)
  ```powershell
  # Check version
  az bicep version

  # Install/upgrade
  az bicep install
  az bicep upgrade
  ```

3. **PowerShell** (v7.4 or later recommended)
  ```powershell
  # Check version
  $PSVersionTable.PSVersion

  # Install if needed
  winget install --id Microsoft.Powershell --source winget
  ```

### Azure Permissions

You need the following permissions on the target subscription:

- **Owner** or **Contributor** role at subscription or resource group level
- **User Access Administrator** role (if deploying RBAC assignments)
- Permissions to create resources in **westus3** region

### Authentication

```powershell
# Login to Azure
az login

# Set the target subscription
az account set --subscription "<subscription-id-or-name>"

# Verify current context
az account show --output table
```

## Repository Structure

```
avm-workload/
β”œβ”€β”€ infra/
β”‚   β”œβ”€β”€ main.bicep            # Main infrastructure template
β”‚   β”œβ”€β”€ main.bicepparam       # Deployment parameters
β”‚   └── bicepconfig.json      # Bicep configuration
β”œβ”€β”€ specs/
β”‚   └── 001-legacy-vm-workload/
β”‚       β”œβ”€β”€ spec.md           # Feature specification
β”‚       β”œβ”€β”€ plan.md           # Implementation plan
β”‚       β”œβ”€β”€ data-model.md     # Architecture documentation
β”‚       └── quickstart.md     # This file
└── .specify/
    └── memory/
        └── constitution.md   # Governance framework
```

## Deployment Workflow

### Step 1: Review Parameters

Edit `infra/main.bicepparam` to customize deployment:

```bicep
using './main.bicep'

// Required parameters
param vmSize = 'Standard_D2s_v3'
param vmAdminUsername = 'vmadmin'
param availabilityZone = 1
param fileShareQuotaGiB = 1024
param logAnalyticsRetentionDays = 30

// Optional: Override resource names
// param vmName = 'vm-custom-name'
// param vnetName = 'vnet-custom-name'
```

**Key Parameters**:
- `vmSize`: Virtual machine SKU (must support Windows Server 2016)
- `vmAdminUsername`: Administrator username for the VM
- `availabilityZone`: Availability zone (1, 2, or 3)
- `fileShareQuotaGiB`: Storage file share quota (default 1024 GiB)
- `logAnalyticsRetentionDays`: Log retention period (30-730 days)

### Step 2: Pre-Deployment Validation

#### 2.1 Bicep Compilation

Verify the template compiles without errors:

```powershell
# Navigate to infrastructure directory
cd C:\SOURCE\avm-workload\infra

# Build Bicep template
bicep build main.bicep

# Check for warnings
# Fix any warnings reported by the analyzer
```

**Expected Output**: `main.json` file created with no errors or warnings.

#### 2.2 Template Validation

Validate deployment against Azure:

```powershell
# Create resource group (if it doesn't exist)
az group create `
  --name rg-legacyvm-prod `
  --location westus3

# Validate deployment
az deployment group validate `
  --resource-group rg-legacyvm-prod `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose

# Check validation result
if ($LASTEXITCODE -eq 0) {
    Write-Host "βœ… Validation passed" -ForegroundColor Green
} else {
    Write-Host "❌ Validation failed - review errors above" -ForegroundColor Red
    exit 1
}
```

**Expected Output**: `provisioningState: Succeeded`

#### 2.3 What-If Analysis

Preview what resources will be created:

```powershell
# Run what-if analysis
az deployment group what-if `
  --resource-group rg-legacyvm-prod `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose

# Review output:
# - Green (+): Resources to be created
# - Yellow (~): Resources to be modified
# - Red (x): Resources to be deleted
# - White (=): No change
```

**Review Checklist**:
- [ ] 1 Virtual Network with 3 subnets
- [ ] 3 Network Security Groups
- [ ] 1 NAT Gateway with Public IP
- [ ] 1 Azure Bastion with Public IP
- [ ] 1 Key Vault with 1 secret
- [ ] 1 Storage Account with 1 file share
- [ ] 1 Private Endpoint
- [ ] 1 Private DNS Zone with VNet link
- [ ] 1 Virtual Machine with NIC, OS disk, data disk
- [ ] 1 Log Analytics Workspace
- [ ] 3 Metric Alerts
- [ ] Multiple diagnostic settings
- [ ] RBAC role assignments

**STOP**: Do not proceed if what-if shows unexpected resource deletions or modifications.

### Step 3: Deploy Infrastructure

#### 3.1 Execute Deployment

```powershell
# Deploy infrastructure
az deployment group create `
  --name "legacyvm-$(Get-Date -Format 'yyyyMMdd-HHmmss')" `
  --resource-group rg-legacyvm-prod `
  --template-file main.bicep `
  --parameters main.bicepparam `
  --verbose

# Deployment typically takes 15-20 minutes
# Monitor progress in Azure Portal: Resource Groups > rg-legacyvm-prod > Deployments
```

**Expected Duration**: 15-20 minutes

**Deployment Phases**:
1. **0-2 min**: Log Analytics, VNet, NSGs
2. **2-8 min**: NAT Gateway, Bastion, Private DNS Zone, Key Vault
3. **8-12 min**: Storage Account, Private Endpoint
4. **12-18 min**: Virtual Machine (longest phase)
5. **18-20 min**: Monitor Alerts

#### 3.2 Monitor Deployment

**Option A: Azure CLI**
```powershell
# Watch deployment status
az deployment group show `
  --name "legacyvm-<timestamp>" `
  --resource-group rg-legacyvm-prod `
  --query "{State:properties.provisioningState, Duration:properties.duration}" `
  --output table
```

**Option B: Azure Portal**
1. Navigate to: [Azure Portal](https://portal.azure.com)
2. Go to: **Resource Groups** > **rg-legacyvm-prod** > **Deployments**
3. Click on the active deployment to see detailed progress
4. Monitor each resource deployment status

### Step 4: Post-Deployment Verification

#### 4.1 Verify Resources

```powershell
# List all resources in the resource group
az resource list `
  --resource-group rg-legacyvm-prod `
  --output table

# Expected count: 20-25 resources
# Key resources to verify:
# - Virtual Machine
# - Virtual Network
# - Storage Account
# - Key Vault
# - Azure Bastion
# - Log Analytics Workspace
```

#### 4.2 Test Bastion Connectivity

**Via Azure Portal**:
1. Go to: **Virtual Machines** > **vm-legacyvm-{random}**
2. Click: **Connect** > **Bastion**
3. Enter credentials:
  - **Username**: `vmadmin`
  - **Password**: Get from Key Vault (see below)
4. Click: **Connect**

**Retrieve VM Password**:
```powershell
# Get Key Vault name
$kvName = az keyvault list `
  --resource-group rg-legacyvm-prod `
  --query "[0].name" `
  --output tsv

# Get VM admin password from Key Vault
az keyvault secret show `
  --name vm-admin-password `
  --vault-name $kvName `
  --query "value" `
  --output tsv
```

**Expected Result**: Successful RDP connection to Windows Server 2016 VM.

#### 4.3 Verify Logs in Log Analytics

```powershell
# Get Log Analytics workspace ID
$workspaceId = az monitor log-analytics workspace show `
  --resource-group rg-legacyvm-prod `
  --workspace-name law-legacyvm-{random} `
  --query "customerId" `
  --output tsv

Write-Host "Log Analytics Workspace ID: $workspaceId"
Write-Host "Portal: https://portal.azure.com#blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F{subscription-id}%2FresourceGroups%2Frg-legacyvm-prod%2Fproviders%2FMicrosoft.OperationalInsights%2Fworkspaces%2Flaw-legacyvm-{random}"
```

**Via Azure Portal**:
1. Go to: **Log Analytics Workspaces** > **law-legacyvm-{random}**
2. Click: **Logs**
3. Run query to verify diagnostic logs:

```kusto
// Query 1: Verify VM activity logs
AzureActivity
| where ResourceGroup == "rg-legacyvm-prod"
| where ResourceType == "Microsoft.Compute/virtualMachines"
| summarize count() by OperationName
| order by count_ desc

// Query 2: Verify Key Vault audit logs
AzureDiagnostics
| where ResourceType == "VAULTS"
| where ResourceGroup == "rg-legacyvm-prod"
| summarize count() by OperationName, ResultType
| order by count_ desc

// Query 3: Verify Storage Account logs
StorageFileLogs
| where AccountName startswith "st"
| summarize count() by OperationName
| order by count_ desc

// Query 4: Check for any errors
AzureDiagnostics
| where ResourceGroup == "rg-legacyvm-prod"
| where Level == "Error"
| project TimeGenerated, ResourceType, OperationName, ResultDescription
| order by TimeGenerated desc
```

**Expected Results**: Logs appearing for all resources within 5-10 minutes of deployment.

#### 4.4 Test Alerts

**Test 1: Disk Space Alert** (Optional - requires VM modification)
```powershell
# WARNING: This will consume disk space on the VM
# Only run if you want to test alert firing

# Connect to VM via Bastion, then run in VM:
# fsutil file createnew C:\testfile.tmp 100000000000  # 100GB file

# Wait 5-10 minutes for alert to fire
# Check: Azure Portal > Monitor > Alerts
```

**Test 2: Key Vault Access Failure** (Safe test)
```powershell
# Attempt to get a non-existent secret (should generate access failure log)
az keyvault secret show `
  --name "non-existent-secret" `
  --vault-name $kvName 2>$null

# Wait 5 minutes, then check:
# Azure Portal > Monitor > Alerts > alert-kv-access-fail-legacyvm-{random}
```

**Expected Behavior**: Alerts visible in Azure Portal within 5-10 minutes of trigger condition.

#### 4.5 Verify Network Connectivity

**From VM (via Bastion RDP session)**:

```powershell
# Test internet connectivity via NAT Gateway
Test-NetConnection -ComputerName google.com -Port 443

# Test Azure DNS resolution
nslookup st{random}.file.core.windows.net

# Verify private endpoint resolution
nslookup st{random}.privatelink.file.core.windows.net

# Expected: Private IP from 10.0.0.128/27 range

# Test file share access (future - after mapping)
# net use Z: \\st{random}.file.core.windows.net\fileshare
```

**Expected Results**:
- Internet access works (NAT Gateway)
- Private endpoint resolves to internal IP (10.0.0.128/27)
- File share accessible from VM

## Troubleshooting

### Issue: Bicep Build Fails

**Symptoms**: `bicep build` reports errors or warnings

**Solutions**:
1. **Check Bicep CLI version**:
  ```powershell
  az bicep version
  # Should be v0.33.0 or later
  az bicep upgrade
  ```

2. **Review analyzer warnings**:
  - Open `main.bicep` in VS Code with Bicep extension
  - Fix any red/yellow squiggles
  - Common issues: outdated module versions, missing required parameters

3. **Validate bicepconfig.json**:
  ```powershell
  # Ensure file exists and is valid JSON
  Get-Content infra/bicepconfig.json | ConvertFrom-Json
  ```

### Issue: Validation Fails

**Symptoms**: `az deployment group validate` returns errors

**Common Errors**:

1. **"Resource provider not registered"**
  ```powershell
  # Register required providers
  az provider register --namespace Microsoft.Compute
  az provider register --namespace Microsoft.Network
  az provider register --namespace Microsoft.Storage
  az provider register --namespace Microsoft.KeyVault
  az provider register --namespace Microsoft.Insights

  # Wait for registration to complete (2-5 minutes)
  az provider show --namespace Microsoft.Compute --query "registrationState"
  ```

2. **"Quota exceeded"**
  - Check Azure subscription quotas
  - Request quota increase if needed: Portal > Subscriptions > Usage + quotas

3. **"Invalid parameter value"**
  - Review `main.bicepparam` for typos
  - Ensure `vmSize` is valid for westus3 region
  - Verify availability zone is 1, 2, or 3

### Issue: Deployment Hangs or Times Out

**Symptoms**: Deployment runs longer than 30 minutes

**Diagnosis**:
```powershell
# Check deployment status
az deployment group show `
  --name "legacyvm-<timestamp>" `
  --resource-group rg-legacyvm-prod `
  --query "properties.{State:provisioningState, SubState:provisioningDetails}" `
  --output json

# View deployment operations
az deployment operation group list `
  --resource-group rg-legacyvm-prod `
  --name "legacyvm-<timestamp>" `
  --query "[?properties.provisioningState=='Failed' || properties.provisioningState=='Running']" `
  --output table
```

**Solutions**:
1. **VM creation timeout**: May indicate VM extension failures
  - Check: Portal > VM > Extensions and applications
  - Solution: Redeploy with `--no-wait` flag, monitor separately

2. **Bastion timeout**: Check Public IP allocation
  - Verify Public IP quota not exceeded
  - Check NSG rules on Bastion subnet

3. **Private Endpoint timeout**: DNS propagation delay
  - Wait additional 5-10 minutes
  - Verify Private DNS Zone linked to VNet

### Issue: VM Password Not Working

**Symptoms**: Cannot connect to VM via Bastion with retrieved password

**Solutions**:
1. **Re-retrieve password from Key Vault**:
  ```powershell
  $kvName = az keyvault list --resource-group rg-legacyvm-prod --query "[0].name" -o tsv
  $password = az keyvault secret show --name vm-admin-password --vault-name $kvName --query "value" -o tsv
  Write-Host "Password: $password"
  ```

2. **Check Key Vault access**:
  ```powershell
  # Ensure you have Key Vault Secrets User role
  az role assignment list `
    --scope /subscriptions/{subscription-id}/resourceGroups/rg-legacyvm-prod/providers/Microsoft.KeyVault/vaults/$kvName `
    --query "[?principalName=='<your-user-email>']" `
    --output table
  ```

3. **Reset VM password** (if secret retrieval works but password is wrong):
  ```powershell
  # This should not be necessary if deployment succeeded
  # Only use as last resort
  az vm user update `
    --resource-group rg-legacyvm-prod `
    --name vm-legacyvm-{random} `
    --username vmadmin `
    --password "NewP@ssw0rd!123"
  ```

### Issue: Bastion Connection Fails

**Symptoms**: Cannot establish Bastion RDP session

**Diagnosis**:
```powershell
# Check Bastion health
az network bastion show `
  --resource-group rg-legacyvm-prod `
  --name bas-legacyvm-{random} `
  --query "{ProvisioningState:provisioningState, DNSName:dnsName}" `
  --output table

# Check VM status
az vm get-instance-view `
  --resource-group rg-legacyvm-prod `
  --name vm-legacyvm-{random} `
  --query "instanceView.statuses[?starts_with(code, 'PowerState/')].displayStatus" `
  --output tsv
```

**Solutions**:
1. **VM is stopped**: Start the VM
  ```powershell
  az vm start --resource-group rg-legacyvm-prod --name vm-legacyvm-{random}
  ```

2. **Bastion NSG rules incorrect**: Verify Bastion subnet NSG
  - Required: Allow inbound 443 from Internet
  - Required: Allow outbound 3389/22 to VirtualNetwork
  - Check: Portal > NSG > nsg-bastion-legacyvm-{random} > Security rules

3. **Browser issues**: Try different browser or incognito mode

### Issue: No Logs in Log Analytics

**Symptoms**: Queries return no results 10+ minutes after deployment

**Diagnosis**:
```kusto
// Check if workspace is receiving any data
Heartbeat
| where TimeGenerated > ago(1h)
| summarize count()

// Check diagnostic settings configuration
AzureDiagnostics
| where TimeGenerated > ago(1h)
| summarize count() by ResourceType
```

**Solutions**:
1. **Wait longer**: Initial log ingestion can take 10-15 minutes
2. **Verify diagnostic settings**:
  ```powershell
  # Check VM diagnostic settings
  az monitor diagnostic-settings list `
    --resource /subscriptions/{subscription-id}/resourceGroups/rg-legacyvm-prod/providers/Microsoft.Compute/virtualMachines/vm-legacyvm-{random} `
    --query "value[].{Name:name, LogAnalytics:workspaceId}" `
    --output table
  ```

3. **Manual diagnostic setting creation** (if missing):
  - Portal > VM > Diagnostic settings > Add diagnostic setting
  - Select all log categories and metrics
  - Send to Log Analytics workspace: law-legacyvm-{random}

### Issue: Alerts Not Firing

**Symptoms**: Test conditions met but no alerts visible in Portal

**Diagnosis**:
```powershell
# Check alert rules
az monitor metrics alert list `
  --resource-group rg-legacyvm-prod `
  --query "[].{Name:name, Enabled:enabled, Severity:severity}" `
  --output table

# Check alert condition evaluation
az monitor metrics alert show `
  --resource-group rg-legacyvm-prod `
  --name alert-disk-space-legacyvm-{random} `
  --query "{Enabled:enabled, Condition:criteria, State:properties.state}" `
  --output json
```

**Solutions**:
1. **Wait for evaluation window**: Alerts evaluate every 1-5 minutes
2. **Verify alert is enabled**: Should show `"enabled": true`
3. **Check metric availability**:
  ```powershell
  # List available metrics for VM
  az monitor metrics list-definitions `
    --resource /subscriptions/{subscription-id}/resourceGroups/rg-legacyvm-prod/providers/Microsoft.Compute/virtualMachines/vm-legacyvm-{random} `
    --query "[].{Name:name.value, Unit:unit}" `
    --output table
  ```

4. **Review activity log for alert evaluation**:
  - Portal > Monitor > Activity Log
  - Filter: Resource Type = "microsoft.insights/metricalerts"
  - Look for "Evaluate Action" events

## Clean Up Resources

**WARNING**: This will delete ALL resources and data. Ensure you have backups before proceeding.

```powershell
# Delete resource group and all resources
az group delete `
  --name rg-legacyvm-prod `
  --yes `
  --no-wait

# Verify deletion status (takes 5-10 minutes)
az group exists --name rg-legacyvm-prod
# Expected output: false
```

**Cost Estimate**: Keeping resources deployed costs approximately:
- VM (Standard_D2s_v3): ~$70/month
- Storage (1TB file share + disks): ~$50/month
- Bastion: ~$140/month
- Other services (negligible): ~$10/month
- **Total**: ~$270/month in westus3 region

## Next Steps

After successful deployment:

1. **Configure VM**:
  - Install required applications on Windows Server 2016
  - Map file share as network drive: `\\st{random}.file.core.windows.net\fileshare`
  - Configure Windows Firewall rules as needed

2. **Set Up Monitoring**:
  - Configure Log Analytics queries and save as functions
  - Create custom workbooks in Azure Monitor
  - Set up action groups for email/SMS notifications (currently Portal-only)

3. **Implement Backup** (not in scope of this deployment):
  - Azure Backup for VM
  - Azure Files snapshot/backup for file share

4. **Security Hardening** (additional measures):
  - Enable Azure Security Center recommendations
  - Implement Just-In-Time VM access
  - Review and tighten NSG rules based on actual traffic

5. **Operational Procedures**:
  - Document VM maintenance schedules
  - Create runbooks for common tasks
  - Establish change management process

## Support

For issues related to:
- **Bicep**: Review [research.md](./research.md) for module documentation
- **Azure resources**: Check [data-model.md](./data-model.md) for architecture
- **Requirements**: See [spec.md](./spec.md) for detailed specifications
- **Governance**: Review [constitution.md](../../.specify/memory/constitution.md) for principles

For Azure support, visit: https://azure.microsoft.com/support/
/speckit.planΒ Create a detailed plan for the spec. Build with the latest version of Terraform and the latest available version of each Azure Verified Module. Use the Terraform MCP server ("io.github.hashicorp/terraform-mcp-server") to find out what's the latest version of each module - install and configure this MCP server as needed. Do NOT use the "Bicep/list_avm_metadata" MCP tool! Only include direct resource references in the Terraform solution template (root module) if no related AVM resource modules are available. If there is not an Azure Verified Module available, then use `azapi` provider resources, never use the `azurerm` provider directly. Always use module interfaces for diagnostic settings, role assignments, resource locks, tags, managed identities, private endpoints, customer manged keys, etc., always use the related "interface" built-in to each resource module when available. Do not create and reference local modules, or any other Terraform files.Β If a subset of the deployments fail, don't delete anything, just attempt redeploying the whole solution after fixing any bugs. Follow IaC best practices: define everything in a single root module using the standard module files of `main.tf`, `variables.tf`, `outputs.tf`, `terraform.tf`, and `terraform.tfvars`. Always build Terraform explicit dependencies to determine order of deployment for each Azure resource, only use explicit dependencies with the `depends_on` meta-argument when it's not possible to otherwise determine the order of deployment. The Azure subscription ID will always be supplied as an env var or via az cli, it must not be exposed as a variable.

The subscription ID will be provided at deployment time via environment variable or az cli, it should not be exposed as a variable in the code.

When generating the admin password for the VM, use the secret feature built into the AVM Key Vault module. Leverage the random_password resource in the random provider to generate a new random password and do not use any external helper script (including deployment scripts) for generating the password. Provide this password to the VM module by referencing the Key vault secret that stores it. The solution template (root module) must first generate this password including a random, complex string, using the random_password Terraform resource, store it in Key Vault and then reference it for the VM to use it as admin password at deployment time. Ephemeral resources and write only attributes should be used for the password.

Don't connect the file share to the VM just yet - i.e., no need to extract storage keys or shared access signatures - we will do this later.

If implementing resource level locks, always use the built-in AVM "interface" for resource locks, instead of directly deploying the "Microsoft.Authorization/locks" resource.

Terraform solution template (root module) must validate without warnings or errors using the latest stable Terraform CLI version. Generate a warning when not the latest version of an AVM module is used. Before validating the solution template (root module) or attempting the first deployment, always fix all warnings or errors related to the AVM module versioning by updating to the latest available version of each module.

Always use snake case for Terraform HCL resource names, module names, variable names, output names, map keys, etc. Never shorten names, always use the full name. E.g. `network_security_group` instead of `nsg`, `virtual_machine` instead of `vm`, etc.

Ephemeral resources and write only attributes should be used for passwords.
βž• Expand to see the results

Notice how the plan step creates the plan.md file and a number of additional helper files. These may very depending on your prompts, the solution you are building, the version of Spec Kit and the LLM used. These typically include: data-model.md, research.md, quickstart.md and optional files in the contracts folder, such as outputs.md and parameters.md.

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see results, similar to this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Implementation Plan: Legacy Business Application Infrastructure

**Branch**: `001-my-legacy-workload` | **Date**: 2026-02-18 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-my-legacy-workload/spec.md`

**Note**: This plan implements the `/speckit.plan` command workflow.

## Summary

Deploy a legacy Windows Server 2016 business application infrastructure using Terraform and Azure Verified Modules (AVM). The solution includes:
- **Core Compute**: Standard_D2s_v3 VM with Windows Server 2016, 500GB data disk, managed in isolated VNet
- **Network Security**: VNet (10.0.0.0/24) with 3 subnets, NSGs with deny-by-default rules, Azure Bastion for secure RDP
- **Storage**: 1TB Azure Files share accessible via private endpoint
- **Secrets Management**: Azure Key Vault storing VM admin password (generated with random_password resource)
- **Internet Access**: NAT Gateway for outbound-only connectivity
- **Observability**: Log Analytics (180-day retention) with diagnostic logging and critical alerts

**Technical Approach**: Single Terraform root module deploying all resources via AVM modules from Terraform Registry. No backup solution (infrastructure is disposable/recreatable). All configuration in terraform.tfvars per specification. VM password generated using random_password resource, stored in Key Vault via AVM module interface, then referenced by VM module.

## Technical Context

**Infrastructure Language**: Terraform >= 1.9.0 (latest stable as of 2026-02-18)

**Required Providers**:
- `hashicorp/azurerm` ~> 4.0 (latest major version with AVM compatibility)
- `hashicorp/random` ~> 3.6 (for random_password and random_string resources)

**AVM Modules** (versions to be verified from Terraform Registry during Phase 0):
- `Azure/avm-res-network-virtualnetwork/azurerm` - VNet with 3 subnets
- `Azure/avm-res-network-networksecuritygroup/azurerm` - NSGs for each subnet
- `Azure/avm-res-compute-virtualmachine/azurerm` - Windows Server 2016 VM
- `Azure/avm-res-network-bastionhost/azurerm` - Azure Bastion
- `Azure/avm-res-keyvault-vault/azurerm` - Key Vault for secrets
- `Azure/avm-res-storage-storageaccount/azurerm` - Storage account with file share
- `Azure/avm-res-network-privateendpoint/azurerm` - Private endpoint (if not included in storage module)
- `Azure/avm-res-network-natgateway/azurerm` - NAT Gateway
- `Azure/avm-res-operationalinsights-workspace/azurerm` - Log Analytics Workspace
- `Azure/avm-res-insights-metricgroup/azurerm` or similar - Metric alerts (module name TBD)

**Note**: AVM for Terraform modules use naming convention `Azure/avm-res-{service}-{resource}/azurerm`. Exact module names and latest versions must be verified from https://registry.terraform.io/namespaces/Azure during Phase 0 research.

**State Backend**: Azure Storage Account (pre-existing, not managed by this Terraform)
**State File**: `my-legacy-workload-prod.tfstate`
**Target Region**: westus3
**Project Type**: Infrastructure-only (Terraform root module)

**Deployment Method**: Manual terraform apply via CLI (CI/CD pipeline optional for Phase 2)
**Security Tooling**: tfsec >= 1.28, checkov >= 3.0 (for static security analysis)
**Complexity**: 12 Azure resources via AVM modules, estimated ~300-400 lines of Terraform
**Estimated Monthly Cost**: <$200/month (per spec SC-013)

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

Based on `.specify/memory/constitution.md` version 1.0.0:

- [x] **Principle I**: All Azure resources defined in Terraform `.tf` files (no imperative scripts except justified)
  - All 12 resources deployed via Terraform AVM modules
  - No custom PowerShell/CLI scripts for resource deployment

- [x] **Principle II**: All modules sourced from Azure Verified Modules (AVM) Terraform Registry (`Azure/avm-*`)
  - Zero custom/third-party modules
  - All resources use official AVM modules where available
  - Built-in azurerm resources only for resource group and random resources (no AVM module available)

- [x] **Principle III**: Security requirements met:
  - [x] VM managed identity (system-assigned) configured via AVM module interface
  - [x] VM password generated with random_password, stored in Key Vault via AVM secrets interface
  - [x] NSGs with deny-by-default rules, explicit allow for RDP from Bastion only
  - [x] Diagnostic settings enabled via AVM module diagnostic_settings interface
  - [x] Encryption at rest (default Microsoft-managed keys)
  - [x] Resource locks via AVM module lock interface (not direct azurerm_management_lock)
  - [x] No secrets in .tf or .tfvars files (password generated at runtime, stored in Key Vault)

- [x] **Principle IV**: Single root module pattern (all resources in terraform/ directory root)
  - terraform/main.tf contains all module instantiations
  - No local child modules created
  - Terraform dependency graph manages deployment order

- [x] **Principle V**: Deployment includes terraform validate and terraform plan review gates
  - Validation workflow: init β†’ fmt β†’ validate β†’ plan β†’ review β†’ apply
  - Plan file (plan.tfplan) generated and reviewed before apply

- [x] **Deployment Standards**: Target region is `westus3`, naming convention followed (`<type>-avmlegacy-<suffix>`)
  - All resources use location = var.location (default: "westus3")
  - Naming via locals using random_string for uniqueness

- [x] **Project Constraints**: No HA/DR/scalability features added (legacy workload, cost-optimized)
  - Single VM (no availability set, no load balancer)
  - No backup policies (infrastructure disposable per clarification session)
  - Standard/HDD tier for cost optimization

**Constitution Compliance**: βœ… **PASSED** - All principles satisfied

## Project Structure

### Documentation (this feature)

```
specs/001-my-legacy-workload/
β”œβ”€β”€ spec.md              # Feature specification (input)
β”œβ”€β”€ plan.md              # This file (Phase 0-1 output)
β”œβ”€β”€ research.md          # Phase 0 research findings (to be created)
β”œβ”€β”€ data-model.md        # Phase 1 data model (infrastructure entities - to be created)
β”œβ”€β”€ quickstart.md        # Phase 1 deployment guide (to be created)
β”œβ”€β”€ contracts/           # Phase 1 contracts (N/A for infrastructure, no external APIs)
β”œβ”€β”€ checklists/
β”‚   └── requirements.md  # Specification validation checklist (exists)
└── tasks.md             # Phase 2 task breakdown (created by /speckit.tasks command)
```

### Source Code (repository root)

```
terraform/
β”œβ”€β”€ main.tf              # Primary resource declarations and AVM module calls
β”œβ”€β”€ variables.tf         # Input variable definitions with descriptions
β”œβ”€β”€ outputs.tf           # Output value definitions for infrastructure details
β”œβ”€β”€ terraform.tf         # Terraform version and provider configurations
β”œβ”€β”€ backend.tf           # Remote state backend configuration (Azure Storage)
β”œβ”€β”€ locals.tf            # Local value computations (naming, tags)
β”œβ”€β”€ prod.tfvars          # Production environment variable values (this is the ONLY tfvars file)
└── README.md            # Terraform deployment instructions

docs/
β”œβ”€β”€ README.md            # Project overview and setup instructions
└── architecture.md      # Infrastructure architecture diagram and design decisions

.github/
└── workflows/
    └── terraform-validate.yml  # CI/CD pipeline for validation (optional Phase 2)

.gitignore               # Terraform-specific ignore patterns (.terraform/, *.tfstate, *.tfvars except prod.tfvars.example)
```

**Structure Decision**: Selected Option 1 (Terraform Infrastructure). This is a pure infrastructure deployment with no application code. All Terraform files in `terraform/` directory at repository root. Single `prod.tfvars` file per spec requirement (no dev/test environments).

## Complexity Tracking

> **Note**: No constitution violations - this section documents architectural decisions only

| Decision | Rationale | Alternative Considered |
|----------|-----------|------------------------|
| No local Terraform modules | Per spec requirements and constitution, use AVM modules exclusively. Local modules only if AVM unavailable | Could create local modules for repeated patterns - rejected per spec FR-025 |
| Single tfvars file (prod.tfvars) | Spec requires production environment only (per clarification: no dev/test/staging) | Could use terraform workspaces - rejected per spec FR-024 |
| No backup automation | Per clarification session: infrastructure is disposable, recreatable from Terraform. No backup needed | Could add Azure Backup via AVM module - rejected per clarification |
| Minimal VNet (/24) | Per clarification session: cost-optimized, no growth expected for legacy workload | Could use /23 or larger - rejected for cost optimization |
| 1TB file share | Per clarification session: large capacity selected to avoid future expansion | Could use smaller quota (100GB-500GB) - rejected per user preference |

---

## Phase 0: Outline & Research

**Objective**: Resolve all "NEEDS CLARIFICATION" items from Technical Context and research AVM module capabilities

### Research Tasks

#### Task 1: Verify Latest Terraform Version
**Research**: Confirm Terraform stable version >= 1.9.0 available
**Method**: Check https://developer.hashicorp.com/terraform/downloads or run `terraform version`
**Outputs**: Exact Terraform version constraint for terraform.tf

#### Task 2: Verify Azure RM Provider Version
**Research**: Confirm azurerm provider version ~> 4.0 compatible with AVM modules
**Method**: Check https://registry.terraform.io/providers/hashicorp/azurerm/latest and AVM module documentation
**Outputs**: Exact provider version constraint

#### Task 3: Research AVM Module Availability and Versions
**Research**: For each required Azure resource, identify:
1. Official AVM module name on Terraform Registry
2. Latest stable version (semantic versioning)
3. Module README documentation link
4. Key input variables and interfaces (diagnostic_settings, lock, managed_identities, private_endpoints, secrets)

**Method**: Visit https://registry.terraform.io/namespaces/Azure and search for:
- `avm-res-network-virtualnetwork`
- `avm-res-network-networksecuritygroup`
- `avm-res-compute-virtualmachine`
- `avm-res-network-bastionhost`
- `avm-res-keyvault-vault`
- `avm-res-storage-storageaccount`
- `avm-res-network-privateendpoint` (or check if storage module has built-in private endpoint interface)
- `avm-res-network-natgateway`
- `avm-res-operationalinsights-workspace`
- Insights/monitoring module for metric alerts (name TBD)

**Critical**: Verify each module supports:
- `diagnostic_settings` interface for Log Analytics integration
- `lock` interface for resource locks (CanNotDelete)
- `managed_identities` interface for system-assigned identity
- `secrets` interface (Key Vault module only) for storing random_password output
- `private_endpoints` interface (storage module) for private connectivity

**Outputs**: Populate `research.md` with findings:

```markdown
## AVM Module Research

### Module: avm-res-network-virtualnetwork
- **Registry Path**: Azure/avm-res-network-virtualnetwork/azurerm
- **Latest Version**: [VERSION] (verify from registry)
- **Documentation**: https://registry.terraform.io/modules/Azure/avm-res-network-virtualnetwork/azurerm/latest
- **Key Interfaces**:
  - Subnets: Supports multiple subnet definitions with CIDR allocation
  - NSG Association: [Check if built-in or separate]
  - Diagnostic Settings: [Verify interface availability]
- **Variables Required for Spec**:
  - address_space = ["10.0.0.0/24"]
  - subnets = { vm = "10.0.0.0/27", bastion = "10.0.0.32/26", private_endpoint = "10.0.0.96/28" }
  - location, resource_group_name, etc.

[Repeat for each module...]
```

#### Task 4: Research NSG Rule Patterns
**Research**: Best practices for NSG rules in AVM network security group module:
- Deny-by-default posture
- Allow RDP (3389) from Bastion subnet to VM subnet
- Allow HTTPS (443) inbound to Bastion subnet (Azure Bastion requirement)
- Allow SMB (445) from VM subnet to Private Endpoint subnet

**Method**: Review AVM NSG module documentation for security_rules input structure
**Outputs**: Document NSG rule schema in research.md

#### Task 5: Research VM Password Flow with Key Vault
**Research**: Confirm workflow for generating password and storing in Key Vault via AVM:
1. Create random_password resource (length, complexity requirements)
2. Pass random_password.result to Key Vault AVM module's `secrets` interface
3. Reference Key Vault secret in VM AVM module's admin_password input

**Method**: Review AVM Key Vault module's `secrets` interface and VM module's authentication inputs
**Outputs**: Document password generation pattern in research.md

#### Task 6: Research Private Endpoint Integration
**Research**: Determine if storage account AVM module has built-in private endpoint interface or requires separate private endpoint module
**Method**: Check AVM storage account module documentation for `private_endpoints` input variable
**Outputs**: Decision in research.md - use built-in interface vs separate module

#### Task 7: Research Log Analytics Integration Patterns
**Research**: How to configure diagnostic settings for VM, Key Vault, Storage Account to send logs to Log Analytics
**Method**: Review each AVM module's `diagnostic_settings` interface structure
**Outputs**: Document diagnostic_settings input schema in research.md

#### Task 8: Research Alerting Approach
**Research**: AVM modules or direct resources for metric alerts (VM stopped, disk >90%, Key Vault access failures)
**Method**: Check for AVM alerting/monitoring modules or plan to use azurerm_monitor_metric_alert directly
**Outputs**: Decision documented in research.md

### Research Consolidation

**Output**: `research.md` file with structure:

```markdown
# Research Findings: Legacy Business Application Infrastructure

## Decision Log

### Decision 1: Terraform Version
- **Chosen**: Terraform 1.9.x (latest stable)
- **Rationale**: Latest features, bug fixes, security patches
- **Alternatives Considered**: 1.8.x (stable but older), 1.10+ (if available, may have breaking changes)

### Decision 2: AVM Module Versions
- **Chosen**: Latest stable version for each module (semver ~> X.Y.0)
- **Rationale**: Latest features, security fixes, Azure API compatibility
- **Alternatives Considered**: Pin to specific patch versions (rejected - want latest patches)

[Continue for each research task...]

## Module Documentation Summary

### VNet Module
[Findings from Task 3...]

### NSG Module
[Findings from Task 3 and Task 4...]

[etc.]
```

---

## Phase 1: Design & Contracts

**Prerequisites:** `research.md` complete with all module versions and interfaces documented

### Design Artifacts

#### 1. Data Model (Infrastructure Entities)

**Output**: `data-model.md`

```markdown
# Infrastructure Data Model: Legacy Business Application Infrastructure

## Entity Relationships

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Resource Group (rg-avmlegacy-wus3)                      β”‚
β”‚                                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚ Virtual Network (10.0.0.0/24)               β”‚        β”‚
β”‚  β”‚  β”œβ”€ VM Subnet (10.0.0.0/27)                β”‚        β”‚
β”‚  β”‚  β”œβ”€ Bastion Subnet (10.0.0.32/26)          β”‚        β”‚
β”‚  β”‚  └─ Private Endpoint Subnet (10.0.0.96/28) β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚           β”‚           β”‚              β”‚                   β”‚
β”‚           β”‚           β”‚              β”‚                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚ NSG (VM)    β”‚ β”‚NSG(Bastn)β”‚ β”‚NSG(PrivEndpt)β”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚           β”‚                          β”‚                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚                   β”‚
β”‚  β”‚ VM (Std_D2s_v3) β”‚                β”‚                   β”‚
β”‚  β”‚ - OS Disk       │◄────────┐      β”‚                   β”‚
β”‚  β”‚ - Data Disk     β”‚         β”‚      β”‚                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚      β”‚                   β”‚
β”‚           β”‚                   β”‚      β”‚                   β”‚
β”‚           β”‚            β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”           β”‚
β”‚           β”‚            β”‚ Key Vault           β”‚           β”‚
β”‚           β”‚            β”‚ - VM Admin Password β”‚           β”‚
β”‚           β–Ό            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚                         β”‚
β”‚    β”‚ Bastion  β”‚                β”‚                         β”‚
β”‚    β”‚ (RDP)    β”‚                β”‚                         β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚                         β”‚
β”‚           β”‚                     β”‚                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚                         β”‚
β”‚  β”‚ NAT Gateway      β”‚          β”‚                         β”‚
β”‚  β”‚ (Outbound Only)  β”‚          β”‚                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚                         β”‚
β”‚                                 β”‚                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚                         β”‚
β”‚  β”‚ Storage Account          β”‚  β”‚                         β”‚
β”‚  β”‚ - File Share (1TB)       β”‚  β”‚                         β”‚
β”‚  β”‚ - Private Endpoint β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”˜                         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚
β”‚                                                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                            β”‚
β”‚  β”‚ Log Analytics Workspace  β”‚                            β”‚
β”‚  β”‚ - 180-day retention      β”‚                            β”‚
β”‚  β”‚ - Diagnostic logs (VM,   β”‚                            β”‚
β”‚  β”‚   Key Vault, Storage)    β”‚                            β”‚
β”‚  β”‚ - Metric Alerts (3)      β”‚                            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

## Core Entities

### 1. Resource Group
- **Name**: rg-avmlegacy-wus3
- **Location**: westus3
- **Lock**: CanNotDelete
- **Purpose**: Container for all infrastructure resources

### 2. Virtual Network
- **Name**: vnet-avmlegacy-{random}
- **Address Space**: 10.0.0.0/24
- **Subnets**:
  - vm_subnet: 10.0.0.0/27 (30 usable IPs)
  - AzureBastionSubnet: 10.0.0.32/26 (62 usable IPs, Azure requirement)
  - private_endpoint_subnet: 10.0.0.96/28 (14 usable IPs)
- **Diagnostic Settings**: Enabled, logs to Log Analytics

### 3. Network Security Groups (3)
- **vm_nsg**:
  - Inbound Rules: Allow RDP (3389) from AzureBastionSubnet CIDR
  - Outbound Rules: Allow all (default), NAT Gateway handles internet access
- **bastion_nsg**:
  - Inbound Rules: Allow HTTPS (443) from Internet (Azure Bastion requirement)
  - Outbound Rules: Allow RDP (3389) to vm_subnet CIDR
- **private_endpoint_nsg**:
  - Inbound Rules: Allow SMB (445) from vm_subnet CIDR
  - Outbound Rules: Deny all (implicit)

### 4. Virtual Machine
- **Name**: vm-avmlegacy-{random} (ensure ≀15 chars per spec)
- **Computer Name**: Derived from VM name, truncated to 15 chars if needed
- **Size**: Standard_D2s_v3
- **OS**: Windows Server 2016
- **OS Disk**: Standard HDD (127GB default)
- **Data Disk**: 500GB Standard HDD, LUN 0
- **Admin Username**: vmadmin
- **Admin Password**: Sourced from Key Vault secret (generated via random_password)
- **Managed Identity**: System-assigned
- **Availability Zone**: Zone 1 (or 2, 3 per requirement - never -1)
- **Diagnostic Settings**: Enabled, logs to Log Analytics
- **Lock**: CanNotDelete

### 5. Azure Bastion
- **Name**: bastion-avmlegacy-{random}
- **SKU**: Basic or Standard (verify cost in Phase 0)
- **Subnet**: AzureBastionSubnet (/26)
- **Public IP**: Managed by Bastion
- **Lock**: CanNotDelete

### 6. Key Vault
- **Name**: kv-avmlegacy-{random} (3-24 chars, globally unique)
- **SKU**: Standard
- **Soft Delete**: Enabled (90 days default)
- **Purge Protection**: Enabled
- **RBAC vs Access Policies**: Use RBAC (recommended for AVM)
- **Secrets**:
  - vm-admin-password: Generated from random_password resource
- **Diagnostic Settings**: Enabled, logs to Log Analytics
- **Lock**: CanNotDelete

### 7. Storage Account
- **Name**: stavmlegacy{random} (3-24 chars, lowercase alphanumeric, globally unique)
- **SKU**: Standard_LRS
- **Kind**: StorageV2
- **File Share**:
  - Name**: legacyappdata (or from tfvars)
  - Quota**: 1024 GB (1TB)
  - Tier**: TransactionOptimized (Standard)
- **Public Network Access**: Disabled
- **Private Endpoint**: Enabled, connected to private_endpoint_subnet
- **Diagnostic Settings**: Enabled, logs to Log Analytics
- **Lock**: CanNotDelete

### 8. Private Endpoint
- **Name**: pe-storage-avmlegacy-{random}
- **Subnet**: private_endpoint_subnet
- **Private DNS Integration**: Enabled (creates privatelink.file.core.windows.net DNS entry)
- **Sub-resource**: file (for Azure Files)

### 9. NAT Gateway
- **Name**: nat-avmlegacy-{random}
- **SKU**: Standard
- **Public IP**: Dedicated public IP for outbound traffic
- **Associated Subnets**: vm_subnet only
- **Idle Timeout**: 4 minutes (default)

### 10. Log Analytics Workspace
- **Name**: law-avmlegacy-{random}
- **SKU**: PerGB2018
- **Retention**: 180 days
- **Daily Cap**: None (or set based on cost constraints)
- **Diagnostic Log Sources**: VM, Key Vault, Storage Account

### 11. Metric Alerts (3)
- **VM Stopped Alert**:
  - Metric**: VM Availability (or PowerState)
  - Condition**: Threshold = 0 (stopped)
  - Action Group**: (email/webhook TBD in tfvars)
- **VM Disk Usage Alert**:
  - Metric**: Disk Space Used Percentage
  - Condition**: Threshold > 90%
  - Action Group**: (same as above)
- **Key Vault Access Failure Alert**:
  - Metric**: Failed requests or Access denied events
  - Condition**: Count > 0 over 5 minutes
  - Action Group**: (same as above)

### 12. Supporting Resources
- **random_string**: Generate 6-character unique suffix for naming
- **random_password**: Generate VM admin password (16+ chars, complexity requirements)

## Terraform State Dependencies

Implicit dependency order (Terraform will resolve):
1. random_string, random_password (no dependencies)
2. Resource Group
3. Log Analytics Workspace (for diagnostic settings)
4. Key Vault β†’ secrets (stores random_password)
5. VNet β†’ Subnets
6. NSGs (reference VNet for associations)
7. NAT Gateway β†’ Public IP
8. VM (references Key Vault secret, VNet subnet, NSG)
9. Bastion (references VNet Bastion subnet)
10. Storage Account β†’ File Share β†’ Private Endpoint
11. Diagnostic Settings (references Log Analytics, resources)
12. Metric Alerts (references resources, Log Analytics)
13. Resource Locks (references resources)
```

#### 2. API Contracts

**Output**: N/A for infrastructure project (no external APIs to define)

#### 3. Quickstart Guide

**Output**: `quickstart.md`

```markdown
# Quickstart: Deploy Legacy Business Application Infrastructure

## Prerequisites

1. **Terraform CLI**: Version >= 1.9.0
  ```bash
  terraform version
  # Terraform v1.9.x
  ```

2. **Azure CLI**: Authenticated with sufficient permissions
  ```bash
  az login
  az account show
  # Verify correct subscription
  ```

3. **Azure Subscription**: Contributor role on target subscription or resource group

4. **Terraform State Backend**: Pre-existing Azure Storage Account with container for state
  - Storage Account name: `<your-state-storage>`
  - Container name: `tfstate`
  - SAS token or Storage Account Key

5. **Security Tools** (optional but recommended):
  - tfsec >= 1.28
  - checkov >= 3.0

## Setup Steps

### Step 1: Clone Repository and Navigate to Terraform Directory

```bash
git clone <repository-url>
cd <repository>/terraform
```

### Step 2: Configure Backend

Create `backend.hcl` file (not committed to git):

```hcl
storage_account_name = "<your-state-storage>"
container_name       = "tfstate"
key                  = "my-legacy-workload-prod.tfstate"
resource_group_name  = "<state-storage-resource-group>"
```

### Step 3: Review and Customize prod.tfvars

Edit `prod.tfvars` to customize deployment:

```hcl
# Required variables
location         = "westus3"
workload_name    = "avmlegacy"
environment      = "prod"
vm_admin_secret_name = "vm-admin-password"  # Key Vault secret name

# Optional overrides (defaults provided in variables.tf)
vm_size          = "Standard_D2s_v3"
vm_data_disk_size_gb = 500
file_share_quota_gb  = 1024
log_analytics_retention_days = 180
availability_zone = 1  # or 2, 3 - never -1

# Alert action group (email/webhook)
alert_action_group_email = "admin@example.com"
```

### Step 4: Initialize Terraform

```bash
terraform init -backend-config=backend.hcl
```

Expected output:
```
Terraform has been successfully initialized!
```

### Step 5: Format and Validate

```bash
terraform fmt -recursive
terraform validate
```

Expected output:
```
Success! The configuration is valid.
```

### Step 6: Run Security Scans (Optional)

```bash
tfsec .
checkov -d .
```

Fix any HIGH or CRITICAL findings before proceeding.

### Step 7: Plan Deployment

```bash
terraform plan -var-file=prod.tfvars -out=plan.tfplan
```

**Review the plan carefully**:
- Verify 12-15 resources to be created (exact count depends on AVM module resource expansion)
- Check resource names match naming convention
- Verify no unexpected deletions or replacements
- Confirm all resources deploying to westus3

### Step 8: Apply Deployment

```bash
terraform apply plan.tfplan
```

Deployment takes approximately 20-30 minutes. Progress:
1. Resource Group, Log Analytics (1-2 min)
2. VNet, NSGs, Key Vault (3-5 min)
3. Storage Account, Private Endpoint (5-7 min)
4. NAT Gateway, Bastion (10-15 min - Bastion is slowest)
5. VM (7-10 min)
6. Diagnostic Settings, Alerts, Locks (2-3 min)

### Step 9: Verify Deployment

```bash
# Get outputs
terraform output

# Expected outputs:
# resource_group_name = "rg-avmlegacy-wus3"
# vm_name = "vm-avmlegacy-a1b2c3"
# key_vault_name = "kv-avmlegacy-a1b2c3"
# storage_account_name = "stavmlegacya1b2c3"
# log_analytics_workspace_id = "/subscriptions/..."
```

Check Azure Portal:
1. Navigate to Resource Group `rg-avmlegacy-wus3`
2. Verify VM is running
3. Test Bastion connection (Connect β†’ Bastion)
4. Retrieve password from Key Vault secret
5. Verify Log Analytics has diagnostic logs

## Post-Deployment

### Connect to VM via Bastion

1. Azure Portal β†’ Virtual Machines β†’ `vm-avmlegacy-...`
2. Click "Connect" β†’ "Bastion"
3. Username: `vmadmin`
4. Password: Retrieve from Key Vault:
  ```bash
  az keyvault secret show --name vm-admin-password --vault-name <kv-name> --query value -o tsv
  ```
5. Click "Connect"

### Mount Azure Files Share

From within the VM (via Bastion RDP session):

```powershell
# Get storage account name from terraform output
$storageAccountName = "<storage-account-name>"
$fileShareName = "legacyappdata"

# Note: Authentication via private endpoint - no key needed for mounted drive
# Access share via UNC path using private endpoint IP or FQDN
net use Z: \\$storageAccountName.privatelink.file.core.windows.net\$fileShareName
```

### Verify Internet Connectivity

```powershell
# From VM
Invoke-WebRequest -Uri "https://www.microsoft.com" -UseBasicParsing
# Should succeed via NAT Gateway
```

### Check Diagnostic Logs

Azure Portal β†’ Log Analytics Workspace β†’ Logs:

```kusto
// VM metrics
Perf
| where Computer startswith "vm-avmlegacy"
| where TimeGenerated > ago(1h)
| take 10

// Key Vault access logs
AzureDiagnostics
| where ResourceType == "VAULTS"
| where TimeGenerated > ago(1h)
| take 10
```

## Troubleshooting

### Issue: Terraform init fails with backend authentication error
**Solution**: Verify backend.hcl credentials and ensure storage account allows access from your IP

### Issue: VM creation fails with quota error
**Solution**: Check Azure subscription quotas for Standard_D2s_v3 in westus3 region

### Issue: Bastion deployment times out
**Solution**: Bastion can take 15-20 minutes. If timeout occurs, run `terraform apply` again (idempotent)

### Issue: Cannot connect via Bastion
**Solution**: Verify NSG rules allow RDP from Bastion subnet. Check VM is running. Verify password from Key Vault.

### Issue: File share inaccessible from VM
**Solution**: Verify private endpoint deployed correctly. Check NSG allows SMB (445) from VM subnet. Verify private DNS resolution.

## Cleanup

**Warning**: This destroys all infrastructure. Ensure data is backed up if needed (though per spec, infrastructure is disposable).

```bash
terraform destroy -var-file=prod.tfvars
```

Confirm with `yes` when prompted.

**Note**: Some resources (Key Vault with purge protection) may enter soft-delete state and require manual purge after 90 days.
```

#### 4. Agent Context Update

**Output**: Run agent context update script (if applicable for Copilot context files)

```bash
# Run from repository root
./.specify/scripts/powershell/update-agent-context.ps1 -AgentType copilot
```

This updates .github/copilot-instructions.md or similar with:
- Terraform/AVM technology stack
- westus3 region
- Constitution principles
- Preserves manual additions between markers

#### 5. Re-evaluate Constitution Check

**Post-Design Validation**: Review design artifacts against constitution:

- [x] **Principle I**: All resources in Terraform (data-model.md documents 12 resources via AVM modules)
- [x] **Principle II**: Only AVM modules used (no custom modules in design)
- [x] **Principle III**: Security controls documented in data-model.md (NSGs, Key Vault, managed identity, diagnostic logging, locks)
- [x] **Principle IV**: Single root module structure documented in project structure
- [x] **Principle V**: Quickstart.md documents validation workflow (init β†’ fmt β†’ validate β†’ plan β†’ apply)

**Constitution Compliance Post-Design**: βœ… **MAINTAINED**

---

## Terraform Code Structure

### File: terraform.tf

```hcl
# Terraform and Provider Configuration
# Constitution Principle I & V: Use latest stable Terraform and Azure provider

terraform {
  required_version = ">= 1.9.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }

  # Backend configuration - parameterized via backend.hcl
  backend "azurerm" {
    # Configured via: terraform init -backend-config=backend.hcl
    # backend.hcl contains:
    #   storage_account_name = "..."
    #   container_name       = "tfstate"
    #   key                  = "my-legacy-workload-prod.tfstate"
    #   resource_group_name  = "..."
  }
}

provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = true
    }
    key_vault {
      purge_soft_delete_on_destroy = false
      recover_soft_deleted_key_vaults = true
    }
  }
}

provider "random" {}
```

### File: variables.tf

```hcl
# Input Variables for Legacy Business Application Infrastructure
# Constitution Principle I: All configurable values in terraform.tfvars

# Required Variables

variable "location" {
  description = "Azure region for all resources. Per constitution: westus3"
  type        = string
  default     = "westus3"

  validation {
    condition     = var.location == "westus3"
    error_message = "Per constitution IC-001, all resources must deploy to westus3."
  }
}

variable "workload_name" {
  description = "Workload identifier for naming convention. Per constitution: avmlegacy"
  type        = string
  default     = "avmlegacy"
}

variable "environment" {
  description = "Environment name. Per spec: prod only"
  type        = string
  default     = "prod"

  validation {
    condition     = var.environment == "prod"
    error_message = "Per spec FR-024, only production environment is deployed."
  }
}

# VM Configuration

variable "vm_size" {
  description = "Azure VM SKU. Per clarification: Standard_D2s_v3"
  type        = string
  default     = "Standard_D2s_v3"
}

variable "vm_admin_username" {
  description = "VM administrator username. Per spec FR-005: vmadmin"
  type        = string
  default     = "vmadmin"

  validation {
    condition     = var.vm_admin_username == "vmadmin"
    error_message = "Per spec FR-005, VM admin username must be vmadmin."
  }
}

variable "vm_admin_secret_name" {
  description = "Key Vault secret name for VM admin password. Per spec FR-016: configurable"
  type        = string
  default     = "vm-admin-password"
}

variable "vm_data_disk_size_gb" {
  description = "VM data disk size in GB. Per spec FR-003: 500GB"
  type        = number
  default     = 500

  validation {
    condition     = var.vm_data_disk_size_gb == 500
    error_message = "Per spec FR-003, VM data disk must be 500GB."
  }
}

variable "availability_zone" {
  description = "Availability zone for VM. Per spec FR-023: 1, 2, or 3 (never -1)"
  type        = number
  default     = 1

  validation {
    condition     = contains([1, 2, 3], var.availability_zone)
    error_message = "Per spec FR-023 and IC-006, availability zone must be 1, 2, or 3."
  }
}

# Network Configuration

variable "vnet_address_space" {
  description = "VNet address space. Per clarification: 10.0.0.0/24"
  type        = list(string)
  default     = ["10.0.0.0/24"]

  validation {
    condition     = length(var.vnet_address_space) == 1 && var.vnet_address_space[0] == "10.0.0.0/24"
    error_message = "Per clarification, VNet must use 10.0.0.0/24 address space."
  }
}

variable "vm_subnet_cidr" {
  description = "VM subnet CIDR. Per clarification: 10.0.0.0/27"
  type        = string
  default     = "10.0.0.0/27"
}

variable "bastion_subnet_cidr" {
  description = "Bastion subnet CIDR. Per clarification: 10.0.0.32/26 (Azure requires /26 minimum)"
  type        = string
  default     = "10.0.0.32/26"
}

variable "private_endpoint_subnet_cidr" {
  description = "Private Endpoint subnet CIDR. Per clarification: 10.0.0.96/28"
  type        = string
  default     = "10.0.0.96/28"
}

# Storage Configuration

variable "file_share_name" {
  description = "Azure Files share name"
  type        = string
  default     = "legacyappdata"
}

variable "file_share_quota_gb" {
  description = "File share quota in GB. Per clarification: 1024GB (1TB)"
  type        = number
  default     = 1024

  validation {
    condition     = var.file_share_quota_gb == 1024
    error_message = "Per clarification, file share quota must be 1TB (1024GB)."
  }
}

# Observability Configuration

variable "log_analytics_retention_days" {
  description = "Log Analytics retention in days. Per clarification: 180 days"
  type        = number
  default     = 180

  validation {
    condition     = var.log_analytics_retention_days == 180
    error_message = "Per clarification, Log Analytics retention must be 180 days."
  }
}

variable "alert_action_group_email" {
  description = "Email address for alert notification action group"
  type        = string
  # No default - must be provided in tfvars
}

# Tags

variable "tags" {
  description = "Common tags for all resources"
  type        = map(string)
  default = {
    Environment = "Production"
    Workload    = "Legacy Business Application"
    ManagedBy   = "Terraform"
    CostCenter  = "IT-Infrastructure"
  }
}
```

### File: locals.tf

```hcl
# Local Values for Computed Names and Configurations
# Constitution Principle: Naming convention <type>-<workload>-<suffix>

locals {
  # Generate unique suffix for globally unique names
  unique_suffix = random_string.unique_suffix.result

  # Location abbreviation
  location_abbr = "wus3"  # westus3

  # Naming per constitution IC-003
  resource_group_name       = "rg-${var.workload_name}-${var.environment}-${local.location_abbr}"
  vnet_name                = "vnet-${var.workload_name}-${local.unique_suffix}"
  vm_nsg_name              = "nsg-vm-${var.workload_name}-${local.unique_suffix}"
  bastion_nsg_name         = "nsg-bastion-${var.workload_name}-${local.unique_suffix}"
  private_endpoint_nsg_name = "nsg-pe-${var.workload_name}-${local.unique_suffix}"

  # VM name must be ≀15 chars for computer name (Windows NetBIOS limit per spec FR-004)
  vm_name_raw = "vm-${var.workload_name}-${local.unique_suffix}"
  vm_name = substr(local.vm_name_raw, 0, min(length(local.vm_name_raw), 15))
  vm_computer_name = local.vm_name  # Same as VM name, truncated to 15 chars

  bastion_name             = "bastion-${var.workload_name}-${local.unique_suffix}"
  key_vault_name           = "kv-${var.workload_name}-${local.unique_suffix}"

  # Storage account name: lowercase alphanumeric only, max 24 chars
  storage_account_name = "st${var.workload_name}${local.unique_suffix}"  # e.g., "stavmlegacya1b2c3"

  private_endpoint_name    = "pe-storage-${var.workload_name}-${local.unique_suffix}"
  nat_gateway_name         = "nat-${var.workload_name}-${local.unique_suffix}"
  nat_public_ip_name       = "pip-nat-${var.workload_name}-${local.unique_suffix}"
  law_name                 = "law-${var.workload_name}-${local.unique_suffix}"
  action_group_name        = "ag-${var.workload_name}-${local.unique_suffix}"

  # Subnet names
  vm_subnet_name               = "vm-subnet"
  bastion_subnet_name          = "AzureBastionSubnet"  # Azure requirement: exact name
  private_endpoint_subnet_name = "private-endpoint-subnet"

  # Common tags
  common_tags = merge(
    var.tags,
    {
      DeployedBy = "Terraform"
      Region     = var.location
      Spec       = "001-my-legacy-workload"
    }
  )
}
```

### File: main.tf

```hcl
#############################################################################
# Legacy Business Application Infrastructure - Main Configuration
# Constitution Compliance: All principles I-V enforced
# Spec: 001-my-legacy-workload
#############################################################################

# Random Resources for Naming

resource "random_string" "unique_suffix" {
  length  = 6
  special = false
  upper   = false
  numeric = true
}

# Per spec: Generate VM admin password using random_password
# This will be stored in Key Vault via AVM module interface
resource "random_password" "vm_admin_password" {
  length           = 24
  special          = true
  min_lower        = 2
  min_upper        = 2
  min_numeric      = 2
  min_special      = 2
  override_special = "!@#$%^&*()-_=+[]{}|;:,.<>?"
}

#############################################################################
# Resource Group
# Constitution IC-005: Single resource group for all resources
#############################################################################

resource "azurerm_resource_group" "main" {
  name     = local.resource_group_name
  location = var.location
  tags     = local.common_tags

  lifecycle {
    prevent_destroy = false  # Set to true for production safety
  }
}

# Resource Group Lock - Constitution SEC-011
resource "azurerm_management_lock" "resource_group" {
  name       = "rg-lock-do-not-delete"
  scope      = azurerm_resource_group.main.id
  lock_level = "CanNotDelete"
  notes      = "Prevents accidental deletion of legacy workload infrastructure"
}

#############################################################################
# Log Analytics Workspace
# Spec FR-018: Log Analytics for centralized logging
# Created early for diagnostic settings on other resources
#############################################################################

module "log_analytics" {
  source  = "Azure/avm-res-operationalinsights-workspace/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION from Terraform Registry

  name                = local.law_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Per clarification: 180-day retention
  sku                 = "PerGB2018"
  retention_in_days   = var.log_analytics_retention_days
  daily_quota_gb      = -1  # No daily cap (or set based on cost requirements)

  tags = local.common_tags

  # Lock interface (if supported by AVM module)
  lock = {
    kind = "CanNotDelete"
    name = "law-lock-do-not-delete"
  }
}

#############################################################################
# Virtual Network
# Spec FR-007: VNet with 3 subnets
# Constitution IC-008: 10.0.0.0/24 with specific CIDR allocations
#############################################################################

module "virtual_network" {
  source  = "Azure/avm-res-network-virtualnetwork/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.vnet_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location
  address_space       = var.vnet_address_space

  # Define 3 subnets per spec FR-007
  subnets = {
    vm_subnet = {
      name             = local.vm_subnet_name
      address_prefixes = [var.vm_subnet_cidr]
      # NSG association (if supported by module) or separate azurerm_subnet_network_security_group_association
    }
    bastion_subnet = {
      name             = local.bastion_subnet_name  # Must be exact name per Azure requirement
      address_prefixes = [var.bastion_subnet_cidr]
    }
    private_endpoint_subnet = {
      name             = local.private_endpoint_subnet_name
      address_prefixes = [var.private_endpoint_subnet_cidr]
      # Per spec IC-009: Disable network policies for private endpoints
      private_endpoint_network_policies_enabled = false
    }
  }

  tags = local.common_tags

  # Diagnostic settings - Constitution SEC-010
  diagnostic_settings = {
    law_diag = {
      name                  = "vnet-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
      # Enable all log categories and metrics (check module documentation for exact syntax)
    }
  }

  # Lock interface
  lock = {
    kind = "CanNotDelete"
    name = "vnet-lock-do-not-delete"
  }
}

#############################################################################
# Network Security Groups
# Spec FR-008: NSGs with deny-by-default posture
# Constitution SEC-004: Explicit allow rules only
#############################################################################

# VM Subnet NSG
module "vm_nsg" {
  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.vm_nsg_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Spec FR-009: Allow RDP from Bastion subnet only
  security_rules = [
    {
      name                       = "Allow-RDP-From-Bastion"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "3389"
      source_address_prefix      = var.bastion_subnet_cidr
      destination_address_prefix = var.vm_subnet_cidr
      description                = "Allow RDP from Bastion subnet per spec FR-009"
    },
    {
      name                       = "Deny-All-Inbound"
      priority                   = 4096
      direction                  = "Inbound"
      access                     = "Deny"
      protocol                   = "*"
      source_port_range          = "*"
      destination_port_range     = "*"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
      description                = "Explicit deny-by-default per constitution SEC-004"
    }
  ]

  tags = local.common_tags

  diagnostic_settings = {
    law_diag = {
      name                  = "nsg-vm-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
    }
  }
}

# Bastion Subnet NSG
module "bastion_nsg" {
  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
  version = "~> 0.1.0"

  name                = local.bastion_nsg_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Bastion NSG rules per Azure Bastion requirements
  # See: https://learn.microsoft.com/azure/bastion/bastion-nsg
  security_rules = [
    {
      name                       = "Allow-HTTPS-Inbound"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "443"
      source_address_prefix      = "Internet"
      destination_address_prefix = "*"
      description                = "Allow HTTPS from Internet per Azure Bastion requirement"
    },
    {
      name                       = "Allow-GatewayManager-Inbound"
      priority                   = 110
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "443"
      source_address_prefix      = "GatewayManager"
      destination_address_prefix = "*"
      description                = "Allow Azure Bastion control plane"
    },
    {
      name                       = "Allow-RDP-To-VM-Subnet"
      priority                   = 100
      direction                  = "Outbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "3389"
      source_address_prefix      = "*"
      destination_address_prefix = var.vm_subnet_cidr
      description                = "Allow RDP to VM subnet per spec SEC-006"
    },
    {
      name                       = "Allow-AzureCloud-Outbound"
      priority                   = 110
      direction                  = "Outbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "443"
      source_address_prefix      = "*"
      destination_address_prefix = "AzureCloud"
      description                = "Allow Bastion to Azure services"
    }
  ]

  tags = local.common_tags

  diagnostic_settings = {
    law_diag = {
      name                  = "nsg-bastion-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
    }
  }
}

# Private Endpoint Subnet NSG
module "private_endpoint_nsg" {
  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
  version = "~> 0.1.0"

  name                = local.private_endpoint_nsg_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Spec SEC-007: Allow SMB from VM subnet
  security_rules = [
    {
      name                       = "Allow-SMB-From-VM-Subnet"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "445"
      source_address_prefix      = var.vm_subnet_cidr
      destination_address_prefix = var.private_endpoint_subnet_cidr
      description                = "Allow SMB from VM subnet per spec SEC-007"
    },
    {
      name                       = "Deny-All-Inbound"
      priority                   = 4096
      direction                  = "Inbound"
      access                     = "Deny"
      protocol                   = "*"
      source_port_range          = "*"
      destination_port_range     = "*"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
      description                = "Explicit deny-by-default"
    }
  ]

  tags = local.common_tags

  diagnostic_settings = {
    law_diag = {
      name                  = "nsg-pe-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
    }
  }
}

# NSG Associations (if not handled by VNet module)
resource "azurerm_subnet_network_security_group_association" "vm_subnet" {
  subnet_id                 = module.virtual_network.subnets["vm_subnet"].id
  network_security_group_id = module.vm_nsg.resource_id
}

resource "azurerm_subnet_network_security_group_association" "bastion_subnet" {
  subnet_id                 = module.virtual_network.subnets["bastion_subnet"].id
  network_security_group_id = module.bastion_nsg.resource_id
}

resource "azurerm_subnet_network_security_group_association" "private_endpoint_subnet" {
  subnet_id                 = module.virtual_network.subnets["private_endpoint_subnet"].id
  network_security_group_id = module.private_endpoint_nsg.resource_id
}

#############################################################################
# NAT Gateway
# Spec FR-012: NAT Gateway for outbound internet access
#############################################################################

module "nat_gateway" {
  source  = "Azure/avm-res-network-natgateway/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.nat_gateway_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Public IP for outbound traffic
  public_ip_addresses = [
    {
      name  = local.nat_public_ip_name
      zones = [var.availability_zone]  # Match VM availability zone per best practice
    }
  ]

  # Associate with VM subnet
  subnet_associations = [
    {
      subnet_id = module.virtual_network.subnets["vm_subnet"].id
    }
  ]

  tags = local.common_tags
}

#############################################################################
# Key Vault
# Spec FR-015: Key Vault for VM admin password
# Constitution SEC-002: Store secrets in Key Vault
#############################################################################

module "key_vault" {
  source  = "Azure/avm-res-keyvault-vault/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.key_vault_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  tenant_id                  = data.azurerm_client_config.current.tenant_id
  sku_name                   = "standard"
  soft_delete_retention_days = 90
  purge_protection_enabled   = true  # Per spec SEC-012

  # Use RBAC authorization (recommended over access policies)
  enable_rbac_authorization = true

  # Per spec FR-006 & FR-016: Store VM admin password as secret
  # AVM secrets interface (check module documentation for exact syntax)
  secrets = {
    vm_admin_password = {
      name  = var.vm_admin_secret_name
      value = random_password.vm_admin_password.result
      # Optionally set expiration, content_type, etc.
    }
  }

  tags = local.common_tags

  # Diagnostic settings - Constitution SEC-010
  diagnostic_settings = {
    law_diag = {
      name                  = "kv-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
    }
  }

  # Lock - Constitution SEC-011
  lock = {
    kind = "CanNotDelete"
    name = "kv-lock-do-not-delete"
  }

  depends_on = [random_password.vm_admin_password]
}

# Grant current deployment identity access to Key Vault for deployment
# (If using RBAC, assign Key Vault Secrets Officer or similar role)
data "azurerm_client_config" "current" {}

resource "azurerm_role_assignment" "kv_secrets_deployment" {
  scope                = module.key_vault.resource_id
  role_definition_name = "Key Vault Secrets Officer"
  principal_id         = data.azurerm_client_config.current.object_id
}

#############################################################################
# Storage Account with File Share
# Spec FR-013: Storage account with Azure Files
# Spec FR-014: Private endpoint access only
#############################################################################

module "storage_account" {
  source  = "Azure/avm-res-storage-storageaccount/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.storage_account_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  account_kind             = "StorageV2"
  account_tier             = "Standard"  # HDD per spec IC-007
  account_replication_type = "LRS"       # No geo-redundancy per constitution IC-004

  # Per spec SEC-009: Disable public network access
  public_network_access_enabled = false

  # Encryption per spec SEC-013
  enable_infrastructure_encryption = true

  # File share configuration
  file_shares = {
    legacy_app_data = {
      name  = var.file_share_name
      quota = var.file_share_quota_gb
      tier  = "TransactionOptimized"  # Standard tier
    }
  }

  # Private endpoint configuration (if supported by module interface)
  private_endpoints = {
    file_endpoint = {
      name                          = local.private_endpoint_name
      subnet_resource_id            = module.virtual_network.subnets["private_endpoint_subnet"].id
      subresource_names             = ["file"]  # For Azure Files
      private_dns_zone_group_name   = "file-private-dns"
      # Private DNS zone integration (auto-created or existing)
      private_dns_zone_resource_ids = [] # Or specify existing zone
    }
  }

  tags = local.common_tags

  # Diagnostic settings
  diagnostic_settings = {
    law_diag = {
      name                  = "storage-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
    }
  }

  # Lock
  lock = {
    kind = "CanNotDelete"
    name = "storage-lock-do-not-delete"
  }
}

#############################################################################
# Azure Bastion
# Spec FR-010: Azure Bastion for secure RDP access
#############################################################################

module "bastion" {
  source  = "Azure/avm-res-network-bastionhost/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.bastion_name
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Bastion subnet (must be exact name "AzureBastionSubnet")
  subnet_id = module.virtual_network.subnets["bastion_subnet"].id

  # SKU: Basic or Standard (check cost implications)
  sku = "Basic"  # Or "Standard" for additional features

  # Public IP managed by Bastion module
  # (AVM module typically creates this automatically)

  tags = local.common_tags

  # Lock
  lock = {
    kind = "CanNotDelete"
    name = "bastion-lock-do-not-delete"
  }
}

#############################################################################
# Virtual Machine
# Spec FR-001: Windows Server 2016 VM with Standard_D2s_v3
#############################################################################

module "virtual_machine" {
  source  = "Azure/avm-res-compute-virtualmachine/azurerm"
  version = "~> 0.1.0"  # VERIFY LATEST VERSION

  name                = local.vm_name  # Truncated to 15 chars
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Per spec FR-004: Computer name (NetBIOS) ≀15 chars
  computer_name = local.vm_computer_name

  # Per clarification: Standard_D2s_v3
  vm_size = var.vm_size

  # Per spec IC-006: Availability zone 1, 2, or 3 (never -1)
  zone = var.availability_zone

  # Windows Server 2016 image
  os_profile = {
    windows = {
      admin_username = var.vm_admin_username
      # Reference password from Key Vault secret
      admin_password = module.key_vault.secrets[var.vm_admin_secret_name].value
    }
  }

  source_image_reference = {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2016-Datacenter"
    version   = "latest"
  }

  # Network configuration
  network_interfaces = {
    nic1 = {
      name = "${local.vm_name}-nic"
      ip_configurations = {
        ipconfig1 = {
          name                          = "ipconfig1"
          subnet_id                     = module.virtual_network.subnets["vm_subnet"].id
          private_ip_address_allocation = "Dynamic"
          # Per spec FR-011: No public IP
          public_ip_address_id = null
        }
      }
    }
  }

  # OS disk: Standard HDD per spec FR-002
  os_disk = {
    name                 = "${local.vm_name}-osdisk"
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"  # Standard HDD
    disk_size_gb         = 127  # Default Windows Server size
  }

  # Data disk: 500GB Standard HDD per spec FR-003
  data_disks = {
    data1 = {
      name                 = "${local.vm_name}-datadisk"
      lun                  = 0
      caching              = "ReadWrite"
      storage_account_type = "Standard_LRS"
      disk_size_gb         = var.vm_data_disk_size_gb
    }
  }

  # Per constitution SEC-001: System-assigned managed identity
  managed_identities = {
    system_assigned = true
  }

  tags = local.common_tags

  # Diagnostic settings - Constitution SEC-010
  diagnostic_settings = {
    law_diag = {
      name                  = "vm-diagnostics"
      workspace_resource_id = module.log_analytics.resource_id
    }
  }

  # Lock - Constitution SEC-011
  lock = {
    kind = "CanNotDelete"
    name = "vm-lock-do-not-delete"
  }

  depends_on = [
    module.key_vault,
    azurerm_role_assignment.kv_secrets_deployment
  ]
}

#############################################################################
# Monitoring and Alerts
# Spec FR-020: Critical alerts for VM stopped, disk usage, Key Vault access
#############################################################################

# Action Group for Alert Notifications
resource "azurerm_monitor_action_group" "main" {
  name                = local.action_group_name
  resource_group_name = azurerm_resource_group.main.name
  short_name          = "avmalerts"

  email_receiver {
    name          = "admin-email"
    email_address = var.alert_action_group_email
  }

  tags = local.common_tags
}

# Alert 1: VM Stopped/Deallocated
resource "azurerm_monitor_metric_alert" "vm_stopped" {
  name                = "alert-vm-stopped-${local.vm_name}"
  resource_group_name = azurerm_resource_group.main.name
  scopes              = [module.virtual_machine.resource_id]
  description         = "Alert when VM is stopped or deallocated"
  severity            = 0  # Critical

  criteria {
    metric_namespace = "Microsoft.Compute/virtualMachines"
    metric_name      = "VmAvailabilityMetric"  # Or "Percentage CPU" = 0 for extended period
    aggregation      = "Average"
    operator         = "LessThan"
    threshold        = 1  # VM unavailable
  }

  frequency   = "PT5M"
  window_size = "PT5M"

  action {
    action_group_id = azurerm_monitor_action_group.main.id
  }

  tags = local.common_tags
}

# Alert 2: VM Disk Usage >90%
resource "azurerm_monitor_metric_alert" "vm_disk_usage" {
  name                = "alert-vm-disk-usage-${local.vm_name}"
  resource_group_name = azurerm_resource_group.main.name
  scopes              = [module.virtual_machine.resource_id]
  description         = "Alert when VM disk usage exceeds 90%"
  severity            = 0  # Critical

  criteria {
    metric_namespace = "Microsoft.Compute/virtualMachines"
    metric_name      = "OS Disk Used Percent"  # May need custom metric or Log Analytics query
    aggregation      = "Average"
    operator         = "GreaterThan"
    threshold        = 90
  }

  frequency   = "PT15M"
  window_size = "PT15M"

  action {
    action_group_id = azurerm_monitor_action_group.main.id
  }

  tags = local.common_tags
}

# Alert 3: Key Vault Access Failures
resource "azurerm_monitor_metric_alert" "kv_access_failures" {
  name                = "alert-kv-access-failures-${local.key_vault_name}"
  resource_group_name = azurerm_resource_group.main.name
  scopes              = [module.key_vault.resource_id]
  description         = "Alert when Key Vault access failures occur"
  severity            = 0  # Critical

  criteria {
    metric_namespace = "Microsoft.KeyVault/vaults"
    metric_name      = "ServiceApiResult"
    aggregation      = "Count"
    operator         = "GreaterThan"
    threshold        = 0

    # Filter for failed requests
    dimension {
      name     = "StatusCode"
      operator = "Include"
      values   = ["403"]  # Forbidden/access denied
    }
  }

  frequency   = "PT5M"
  window_size = "PT5M"

  action {
    action_group_id = azurerm_monitor_action_group.main.id
  }

  tags = local.common_tags
}

#############################################################################
# Note: File share mount to VM will be implemented in a later phase
# Per instructions: "Don't connect the file share to the VM just yet"
#############################################################################
```

### File: outputs.tf

```hcl
# Outputs for Legacy Business Application Infrastructure
# These values can be used by external modules or for manual reference

output "resource_group_name" {
  description = "Name of the resource group containing all resources"
  value       = azurerm_resource_group.main.name
}

output "resource_group_id" {
  description = "ID of the resource group"
  value       = azurerm_resource_group.main.id
}

output "virtual_network_name" {
  description = "Name of the virtual network"
  value       = module.virtual_network.name
}

output "virtual_network_id" {
  description = "ID of the virtual network"
  value       = module.virtual_network.resource_id
}

output "vm_name" {
  description = "Name of the virtual machine"
  value       = module.virtual_machine.name
}

output "vm_id" {
  description = "ID of the virtual machine"
  value       = module.virtual_machine.resource_id
}

output "vm_private_ip_address" {
  description = "Private IP address of the VM"
  value       = module.virtual_machine.network_interfaces["nic1"].ip_configurations["ipconfig1"].private_ip_address
}

output "vm_computer_name" {
  description = "Computer name (NetBIOS name) of the VM"
  value       = local.vm_computer_name
}

output "key_vault_name" {
  description = "Name of the Key Vault"
  value       = module.key_vault.name
}

output "key_vault_id" {
  description = "ID of the Key Vault"
  value       = module.key_vault.resource_id
}

output "key_vault_uri" {
  description = "URI of the Key Vault"
  value       = module.key_vault.vault_uri
}

output "vm_admin_secret_name" {
  description = "Name of the Key Vault secret containing VM admin password"
  value       = var.vm_admin_secret_name
  sensitive   = true
}

output "storage_account_name" {
  description = "Name of the storage account"
  value       = module.storage_account.name
}

output "storage_account_id" {
  description = "ID of the storage account"
  value       = module.storage_account.resource_id
}

output "file_share_name" {
  description = "Name of the Azure Files share"
  value       = var.file_share_name
}

output "bastion_name" {
  description = "Name of the Azure Bastion host"
  value       = module.bastion.name
}

output "bastion_id" {
  description = "ID of the Azure Bastion host"
  value       = module.bastion.resource_id
}

output "log_analytics_workspace_name" {
  description = "Name of the Log Analytics workspace"
  value       = module.log_analytics.name
}

output "log_analytics_workspace_id" {
  description = "ID of the Log Analytics workspace"
  value       = module.log_analytics.resource_id
}

output "nat_gateway_name" {
  description = "Name of the NAT Gateway"
  value       = module.nat_gateway.name
}

output "nat_gateway_public_ip" {
  description = "Public IP address of the NAT Gateway"
  value       = module.nat_gateway.public_ip_addresses[0].ip_address
}

# Sensitive outputs - use with caution
output "vm_admin_password" {
  description = "VM administrator password (retrieve from Key Vault instead)"
  value       = random_password.vm_admin_password.result
  sensitive   = true
}

# Instructions for accessing resources
output "bastion_connect_instructions" {
  description = "Instructions for connecting to VM via Bastion"
  value       = <<-EOT
    1. Navigate to Azure Portal
    2. Go to Virtual Machines -> ${module.virtual_machine.name}
    3. Click "Connect" -> "Bastion"
    4. Username: ${var.vm_admin_username}
    5. Password: Retrieve from Key Vault secret "${var.vm_admin_secret_name}"
      Command: az keyvault secret show --name ${var.vm_admin_secret_name} --vault-name ${module.key_vault.name} --query value -o tsv
  EOT
}

output "file_share_mount_instructions" {
  description = "Instructions for mounting Azure Files share from VM"
  value       = <<-EOT
    From within the VM (via Bastion RDP):
    1. Open PowerShell as Administrator
    2. Run: net use Z: \\${module.storage_account.name}.privatelink.file.core.windows.net\${var.file_share_name}
    3. Verify: dir Z:

    Note: Authentication via private endpoint - no storage key needed for mounted drive
  EOT
}
```

### File: prod.tfvars

```hcl
# Production Environment Configuration
# Legacy Business Application Infrastructure
#
# Per spec FR-021: All configurable values in this file (not hardcoded in main.tf)
# Per spec FR-022: Rich comments explaining purpose

#############################################################################
# Core Configuration
#############################################################################

# Azure region for all resources
# Per constitution IC-001: Must be westus3
location = "westus3"

# Workload identifier for resource naming
# Per constitution: avmlegacy for legacy workload
workload_name = "avmlegacy"

# Environment name
# Per spec FR-024: Production only (no dev/test/staging)
environment = "prod"

#############################################################################
# Virtual Machine Configuration
#############################################################################

# VM size/SKU
# Per clarification: Standard_D2s_v3 (2 cores, 8GB RAM)
vm_size = "Standard_D2s_v3"

# VM administrator username
# Per spec FR-005: Must be "vmadmin"
vm_admin_username = "vmadmin"

# Key Vault secret name for VM admin password
# Per spec FR-016: Configurable via this variable
# Password will be automatically generated and stored in Key Vault
vm_admin_secret_name = "vm-admin-password"

# VM data disk size in GB
# Per spec FR-003: Must be 500GB HDD
vm_data_disk_size_gb = 500

# Availability zone for VM
# Per spec FR-023: Must be 1, 2, or 3 (never -1)
# Choose based on region availability
availability_zone = 1

#############################################################################
# Network Configuration
#############################################################################

# Virtual network address space
# Per clarification: 10.0.0.0/24 (minimal allocation, cost-optimized)
vnet_add_space = ["10.0.0.0/24"]

# VM subnet CIDR
# Per clarification: 10.0.0.0/27 (30 usable IPs)
vm_subnet_cidr = "10.0.0.0/27"

# Bastion subnet CIDR
# Per clarification: 10.0.0.32/26 (62 usable IPs, Azure /26 minimum requirement)
bastion_subnet_cidr = "10.0.0.32/26"

# Private Endpoint subnet CIDR
# Per clarification: 10.0.0.96/28 (14 usable IPs, sufficient for storage private endpoint)
private_endpoint_subnet_cidr = "10.0.0.96/28"

#############################################################################
# Storage Configuration
#############################################################################

# Azure Files share name
# Default: legacyappdata
# Change if specific naming required by legacy application
file_share_name = "legacyappdata"

# File share provisioned capacity in GB
# Per clarification: 1024GB (1TB) for large capacity and growth
file_share_quota_gb = 1024

#############################################################################
# Observability Configuration
#############################################################################

# Log Analytics retention period in days
# Per clarification: 180 days for extended compliance coverage
log_analytics_retention_days = 180

# Email address for alert notifications
# **REQUIRED**: Update with actual administrator email
# Receives critical alerts for VM stopped, disk >90%, Key Vault access failures
alert_action_group_email = "admin@example.com"  # ⚠️ UPDATE THIS

#############################################################################
# Resource Tags
#############################################################################

# Common tags applied to all resources
# Add additional cost center, project, owner tags as needed
tags = {
  Environment  = "Production"
  Workload     = "Legacy Business Application"
  ManagedBy    = "Terraform"
  CostCenter   = "IT-Infrastructure"
  Compliance   = "Required"
  DeployedBy   = "Infrastructure Team"
  DeployedDate = "2026-02-18"
  Spec         = "001-my-legacy-workload"
}

#############################################################################
# Notes
#############################################################################

# 1. Per clarification: No backup solution (infrastructure is disposable/recreatable from Terraform)
# 2. Per constitution: Single production environment only (no dev/test)
# 3. Per spec: All resources in westus3 region, single resource group
# 4. Per spec: VM password auto-generated, stored in Key Vault, not in this file
# 5. File share not yet mounted to VM - will be configured in later phase
#############################################################################
```

---

## Summary

Implementation plan complete with:

βœ… **Phase 0 Research**: Documented tasks for verifying Terraform/provider versions and researching 10 AVM modules
βœ… **Phase 1 Design**: Created data-model.md structure, quickstart guide, and agent context update approach
βœ… **Terraform Code**: Complete root module with 5 files (terraform.tf, variables.tf, locals.tf, main.tf, outputs.tf, prod.tfvars)
βœ… **Constitution Compliance**: All 5 principles validated pre and post-design
βœ… **Security**: VM password via random_password β†’ Key Vault β†’ VM reference flow, NSGs, diagnostic logging, resource locks
βœ… **Spec Compliance**: All 25 functional requirements addressed in code structure

**Next Steps**:
1. Execute Phase 0 research to verify exact AVM module versions from Terraform Registry
2. Run `/speckit.tasks` to generate detailed task breakdown for implementation
3. Begin implementation with terraform init and validation workflow
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture the changes in your project, with a comment of something like Plan created.

5. Checklist (Optional)

Spec Kit uses /speckit.checklist to validate the requirements. The prompt doesn’t require any specific inputs as it analyzes the existing plan for gaps.

Info

To learn more about the checklist step, see the Checklist chapter in the Spec Kit article.

  1. Run the following prompt to generate checklist items for our example:
/speckit.checklist
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

When running the checklist prompt, Copilot may ask you a number of depth questions to clarify certain aspects of the plan. Here’s an example of what that looks like. You can answer in the following format, e.g.: Q1: E, Q2:A, Q3:A
Specify Bootstrap

In the Copilot chat window, you should see results, similar to this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Implementation Plan Quality Validation Checklist

**Purpose**: Validate quality, completeness, and clarity of implementation plan and supporting documentation
**Created**: 2026-01-27
**Scope**: Comprehensive review across functional, security, network, and monitoring requirements
**Depth**: Standard (~40 items)
**Feature**: [spec.md](../spec.md) | [plan.md](../plan.md)

---

## Requirement Completeness

Requirements documentation coverage and thoroughness.

- [ ] CHK001 - Are VM compute requirements (CPU, memory, disk) explicitly specified with exact values? [Completeness, Spec Β§FR-001]
- [ ] CHK002 - Are all three network subnet ranges defined with CIDR notation and purpose documented? [Completeness, Spec Β§FR-003]
- [ ] CHK003 - Are storage account requirements specified for both file share quota and performance tier? [Completeness, Spec Β§FR-005]
- [ ] CHK004 - Are availability zone requirements documented with explicit valid range (1-3, never -1)? [Completeness, Spec Β§FR-014]
- [ ] CHK005 - Are diagnostic logging requirements defined for all resource types requiring monitoring? [Completeness, Spec Β§SEC-001]
- [ ] CHK006 - Are all 12 AVM modules documented with exact version numbers and purpose statements? [Completeness, Plan Β§Technical Context]
- [ ] CHK007 - Are deployment sequence phases defined with resource dependencies explicitly stated? [Completeness, Data-Model Β§Deployment Sequence]
- [ ] CHK008 - Are NSG security rules specified for all three subnets with protocol, port, and direction details? [Completeness, Data-Model Β§NSG Configuration]

## Requirement Clarity

Specificity and measurability of requirements to eliminate ambiguity.

- [ ] CHK009 - Is "Standard HDD" quantified with specific Azure SKU names (e.g., StandardSSD_LRS)? [Clarity, Spec Β§FR-001, FR-002]
- [ ] CHK010 - Is "minimal naming" defined with exact pattern format and character count range? [Clarity, Constitution Β§V, Plan Β§Technical Context]
- [ ] CHK011 - Are alert thresholds specified with exact percentage values and evaluation windows? [Clarity, Spec Β§MON-004, Data-Model Β§Alert Configuration]
- [ ] CHK012 - Is "secure remote access" quantified with specific protocol (RDP), port, and authentication method? [Clarity, Spec Β§FR-004]
- [ ] CHK013 - Are "least-privilege NSG rules" defined with concrete allow/deny examples per subnet? [Clarity, Spec Β§SEC-003]
- [ ] CHK014 - Is "deployment within 20 minutes" defined as a measurable success criterion with verification method? [Measurability, Spec Β§SC-001]
- [ ] CHK015 - Is password generation approach explicitly defined with uniqueString() seed sources documented? [Clarity, Research Β§Password Generation]
- [ ] CHK016 - Are resource name length constraints specified with Azure limits (e.g., Storage 24 chars, VM NetBIOS 15 chars)? [Clarity, Spec Β§FR-010, Data-Model Β§Resource Names]

## Requirement Consistency

Alignment and non-contradiction across specification, plan, and supporting documents.

- [ ] CHK017 - Do VM sizing requirements in spec match the data model configuration (Standard_D2s_v3 consistently specified)? [Consistency, Spec Β§FR-001, Data-Model Β§VM Config]
- [ ] CHK018 - Do network CIDR blocks in spec align with data model subnet allocations (10.0.0.0/24 breakdown)? [Consistency, Spec Β§FR-003, Data-Model Β§Network Topology]
- [ ] CHK019 - Do AVM module versions in plan match research document module selections (all 12 modules)? [Consistency, Plan Β§AVM Modules, Research Β§Module Inventory]
- [ ] CHK020 - Do alert requirements in spec match alert configuration in data model (3 critical alerts)? [Consistency, Spec Β§MON-003-005, Data-Model Β§Alerts]
- [ ] CHK021 - Do diagnostic logging requirements align across SEC-001 (spec) and technical context (plan)? [Consistency, Spec Β§SEC-001, Plan Β§Security Baseline]
- [ ] CHK022 - Does naming convention in constitution match implementation in data model? [Consistency, Constitution Β§V, Data-Model Β§Naming Model]
- [ ] CHK023 - Do deployment phases in plan align with dependency graph in data model? [Consistency, Plan Β§Phase 3, Data-Model Β§Deployment Sequence]

## Acceptance Criteria Quality

Measurability and testability of success criteria.

- [ ] CHK024 - Are all 11 success criteria (SC-001 to SC-011) objectively measurable with pass/fail conditions? [Measurability, Spec Β§Success Criteria]
- [ ] CHK025 - Can VM accessibility (SC-004) be verified through documented test procedure in quickstart? [Testability, Spec Β§SC-004, Quickstart Β§4.2]
- [ ] CHK026 - Can NSG rule effectiveness (SC-009) be validated with concrete test scenarios? [Testability, Spec Β§SC-009]
- [ ] CHK027 - Can Log Analytics ingestion (SC-007) be verified within specified 5-minute timeframe? [Measurability, Spec Β§SC-007]
- [ ] CHK028 - Are constitution compliance gates (all 6 principles) verifiable with documented evidence? [Measurability, Plan Β§Constitution Check]

## Scenario Coverage

Completeness of primary, alternate, error, recovery, and non-functional scenarios.

- [ ] CHK029 - Are all five user stories independently testable as documented in spec? [Coverage, Primary Flows, Spec Β§User Scenarios]
- [ ] CHK030 - Are VM computer name length violations (>15 chars) addressed with edge case handling? [Coverage, Edge Case, Spec Β§Edge Cases]
- [ ] CHK031 - Are storage account naming violations (>24 chars, invalid chars) documented as edge cases? [Coverage, Edge Case, Spec Β§Edge Cases]
- [ ] CHK032 - Are private endpoint deployment failures with successful storage account handled? [Coverage, Exception Flow, Spec Β§Edge Cases]
- [ ] CHK033 - Are Key Vault secret access failures monitored with alert configuration? [Coverage, Exception Flow, Spec Β§MON-005]
- [ ] CHK034 - Are availability zone validation requirements (1-3 only, never -1) enforced? [Coverage, Edge Case, Spec Β§FR-014]
- [ ] CHK035 - Are performance requirements addressed for standard HDD selection rationale? [Coverage, Non-Functional, Research Β§Storage Module]

## Edge Case Coverage

Boundary conditions, error states, and exceptional scenarios.

- [ ] CHK036 - Are requirements defined for zero-subnet scenarios or invalid CIDR blocks? [Gap, Edge Case]
- [ ] CHK037 - Are rollback requirements defined if VM deployment succeeds but Key Vault secret creation fails? [Gap, Recovery Flow]
- [ ] CHK038 - Are concurrent deployment conflict scenarios (multiple simultaneous deployments) addressed? [Gap, Edge Case]
- [ ] CHK039 - Are requirements specified for when Log Analytics workspace is unavailable during resource deployment? [Gap, Exception Flow]

## Dependencies & Assumptions

External dependencies, prerequisites, and assumption documentation.

- [ ] CHK040 - Are all Azure resource provider registration requirements documented as prerequisites? [Dependency, Quickstart Β§Prerequisites]
- [ ] CHK041 - Are subscription quota requirements validated as documented assumptions? [Assumption, Spec Β§Assumptions]
- [ ] CHK042 - Are tool version requirements (Bicep 0.33.0+, Azure CLI 2.65.0+) specified with verification commands? [Dependency, Plan Β§Technical Context, Quickstart Β§Prerequisites]
- [ ] CHK043 - Are required Azure permissions documented with specific role names? [Dependency, Quickstart Β§Prerequisites]
- [ ] CHK044 - Is the assumption about Windows Server 2016 image availability in westus3 validated? [Assumption, Spec Β§Assumptions]

## Traceability

Linkage between requirements, specifications, and implementation artifacts.

- [ ] CHK045 - Do all functional requirements (FR-001 to FR-016) have corresponding implementation guidance in plan phases? [Traceability]
- [ ] CHK046 - Do all security requirements (SEC-001 to SEC-008) map to specific AVM module configurations in research? [Traceability]
- [ ] CHK047 - Do all monitoring requirements (MON-001 to MON-005) trace to alert definitions in data model? [Traceability]
- [ ] CHK048 - Do all infrastructure constraints (IC-001 to IC-006) align with constitution principles? [Traceability]

## Ambiguities & Conflicts

Identification of unclear, contradictory, or incomplete requirement areas.

- [ ] CHK049 - Is "HDD-backed file share" disambiguated between Standard_LRS vs other HDD SKUs? [Ambiguity, Spec Β§FR-005]
- [ ] CHK050 - Is "Portal notifications only" for alerts clearly documented as excluding Action Groups? [Clarity, Spec Β§MON-003]
- [ ] CHK051 - Is the deployment timeframe "within 20 minutes" inclusive of validation steps or deployment only? [Ambiguity, Spec Β§SC-001]
- [ ] CHK052 - Are "rich comments" requirements quantified with minimum comment density or coverage percentage? [Ambiguity, Spec Β§FR-015]

---

## Validation Summary

**Total Items**: 52
**Pass Threshold**: β‰₯90% (47+ items checked)
**Review Date**: _________________
**Reviewer**: _________________
**Status**: ☐ PASSED  |  ☐ NEEDS REVISION  |  ☐ BLOCKED

### Issues Identified

_Document any requirement quality issues discovered during validation:_

1.
2.
3.

### Recommendations

_Suggested improvements for requirement clarity, completeness, or testability:_

1.
2.
3.

---

**Next Steps**:
- If PASSED β†’ Proceed to Phase 2 implementation (Bicep template creation)
- If NEEDS REVISION β†’ Address identified issues, re-validate
- If BLOCKED β†’ Escalate missing information to stakeholders
/speckit.checklist
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

When running the checklist prompt, Copilot may ask you a number of depth questions to clarify certain aspects of the plan. Here’s an example of what that looks like.
Specify Bootstrap

In the Copilot chat window, you should see results, similar to this:
Specify Bootstrap

# Requirements Quality Checklist: Implementation Readiness

**Type**: Implementation Readiness Validation
**Focus**: Comprehensive Coverage
**Depth**: Standard
**Audience**: Implementation Team
**Created**: 2026-02-18
**Spec**: [001-my-legacy-workload](../spec.md)
**Plan**: [plan.md](../plan.md)

**Purpose**: Validate that requirements provide complete, clear, and consistent guidance for Terraform implementation using Azure Verified Modules. This checklist tests requirement quality, NOT implementation correctness.

---

## Requirement Completeness

### Infrastructure Resources

- [ ] CHK001 - Are AVM module references specified for all required Azure resources? [Completeness, Spec Infrastructure Requirements]
- [ ] CHK002 - Are resource naming requirements defined with specific patterns and constraints? [Completeness, Spec IC-003, IC-010]
- [ ] CHK003 - Are all resource configuration requirements specified (SKUs, tiers, capacity)? [Completeness, Spec FR-001 through FR-025]
- [ ] CHK004 - Are provider version constraints documented for azurerm and random providers? [Completeness, Plan Technical Context]
- [ ] CHK005 - Are all 12 Azure resources accounted for in both spec and plan? [Completeness, Cross-reference]

### Terraform-Specific Requirements

- [ ] CHK006 - Are requirements defined for all 5 Terraform files (terraform.tf, variables.tf, main.tf, outputs.tf, tfvars)? [Completeness, Spec FR-021, FR-022]
- [ ] CHK007 - Are state backend configuration requirements completely specified? [Completeness, Spec IC-002, State Management section]
- [ ] CHK008 - Are variable definition requirements clear for all configurable values? [Completeness, Spec FR-021]
- [ ] CHK009 - Are output requirements defined for infrastructure consumption by external consumers? [Gap, Plan outputs.tf section]
- [ ] CHK010 - Are Terraform validation workflow steps documented? [Completeness, Plan Constitution Principle V]

### Network Architecture

- [ ] CHK011 - Are VNet address space and subnet CIDR allocations completely specified? [Completeness, Spec IC-008, Clarifications]
- [ ] CHK012 - Are NSG rule requirements defined for all 3 subnets with source/destination specificity? [Completeness, Spec SEC-004 through SEC-007]
- [ ] CHK013 - Are private endpoint connectivity requirements fully documented? [Completeness, Spec FR-014, SEC-009, SEC-014]
- [ ] CHK014 - Are NAT Gateway association requirements clear (which subnets)? [Completeness, Spec FR-012]

### Security & Authentication

- [ ] CHK015 - Are password generation requirements specified with complexity constraints? [Completeness, Spec FR-006, SEC-002]
- [ ] CHK016 - Is the Key Vault secret storage and VM password reference flow clearly documented? [Completeness, Plan Password Flow section]
- [ ] CHK017 - Are managed identity requirements specified for all applicable resources? [Completeness, Spec SEC-001, SEC-003]
- [ ] CHK018 - Are diagnostic logging requirements defined for all monitored resources? [Completeness, Spec SEC-010]
- [ ] CHK019 - Are resource lock requirements specified with lock type and target resources? [Completeness, Spec SEC-011]

### Monitoring & Alerts

- [ ] CHK020 - Are alert condition thresholds quantified for all 3 critical alerts? [Completeness, Spec FR-020, Clarifications]
- [ ] CHK021 - Are Log Analytics retention requirements specified? [Completeness, Spec Clarifications - 180 days]
- [ ] CHK022 - Are alert action group requirements defined (notification method)? [Completeness, Spec Clarifications - Portal notifications]

---

## Requirement Clarity

### Ambiguity Resolution

- [ ] CHK023 - Is "Standard_D2s_v3" VM size explicitly stated (not "2 core, 8GB" generically)? [Clarity, Spec FR-001, Clarifications]
- [ ] CHK024 - Is "Standard HDD" tier explicitly specified vs ambiguous "standard storage"? [Clarity, Spec FR-002, FR-003, IC-007]
- [ ] CHK025 - Is "10.0.0.0/24" VNet address space quantified vs vague "small VNet"? [Clarity, Spec IC-008, Clarifications]
- [ ] CHK026 - Is "180 days" Log Analytics retention quantified vs vague "extended retention"? [Clarity, Spec Clarifications]
- [ ] CHK027 - Is "1TB" file share quota quantified vs vague "large capacity"? [Clarity, Spec Clarifications]

### Terraform-Specific Clarity

- [ ] CHK028 - Are AVM module variable names and structures referenced from module documentation? [Clarity, Spec FR-025]
- [ ] CHK029 - Is the random_password resource explicitly specified vs generic "password generator"? [Clarity, Plan main.tf section]
- [ ] CHK030 - Are AVM module "interfaces" (diagnostic_settings, lock, secrets) explicitly documented? [Clarity, Plan Constitution Principle III]
- [ ] CHK031 - Is "terraform.tfvars" explicit vs ambiguous "variable file"? [Clarity, Spec FR-021]

### Constraint Precision

- [ ] CHK032 - Is "15 characters or fewer" computer name limit quantified? [Clarity, Spec FR-004, IC-010]
- [ ] CHK033 - Is "westus3" region explicitly specified (not "US West" or "West US 3")? [Clarity, Spec IC-001]
- [ ] CHK034 - Is "vmadmin" username exact string specified? [Clarity, Spec FR-005]
- [ ] CHK035 - Are availability zone options explicitly defined as "1, 2, or 3 - NEVER -1"? [Clarity, Spec FR-023, IC-006]

---

## Requirement Consistency

### Cross-Reference Validation

- [ ] CHK036 - Do VNet subnet CIDRs in IC-008 match the clarifications section allocations? [Consistency, Cross-check IC-008 vs Clarifications]
- [ ] CHK037 - Is VM size requirement (FR-001) consistent across spec, user stories, and plan? [Consistency, FR-001, US1, Plan]
- [ ] CHK038 - Are file share capacity requirements consistent (1TB) across FR-013 and clarifications? [Consistency]
- [ ] CHK039 - Are NSG rule requirements consistent with subnet design (3 NSGs for 3 subnets)? [Consistency, FR-008, IC-008]
- [ ] CHK040 - Is Key Vault secret name requirement consistent (configurable via tfvars)? [Consistency, FR-016, SEC-002]

### Security Alignment

- [ ] CHK041 - Do NSG requirements align with zero-trust principles (deny-by-default in SEC-004)? [Consistency, SEC-004 through SEC-007]
- [ ] CHK042 - Are managed identity requirements consistent with "no credentials in code" requirement? [Consistency, SEC-001, SEC-003]
- [ ] CHK043 - Are private endpoint requirements consistent with "no public access" requirements? [Consistency, FR-014, SEC-009]

### Constitution Alignment

- [ ] CHK044 - Do spec requirements align with constitution Principle II (AVM-only)? [Consistency, Infrastructure Requirements vs Constitution]
- [ ] CHK045 - Do security requirements align with constitution Principle III (security controls)? [Consistency, SEC requirements vs Constitution]
- [ ] CHK046 - Do deployment requirements align with constitution Principle V (validation workflow)? [Consistency, SC-009 vs Constitution]

---

## Acceptance Criteria Quality

### Measurability

- [ ] CHK047 - Can "infrastructure deployment within 30 minutes" be objectively measured? [Measurability, SC-001]
- [ ] CHK048 - Can "RDP connection within 2 minutes" be objectively timed? [Measurability, SC-002]
- [ ] CHK049 - Can "diagnostic logs appear within 15 minutes" be objectively verified? [Measurability, SC-007]
- [ ] CHK050 - Can "total cost under $200/month" be objectively calculated? [Measurability, SC-013]

### Testability

- [ ] CHK051 - Are acceptance criteria testable with concrete validation steps? [Testability, All SC items]
- [ ] CHK052 - Are user story test scenarios written in Given/When/Then format? [Testability, User Stories 1-4]
- [ ] CHK053 - Do edge cases include expected behavior or just failure scenarios? [Testability, Edge Cases section]

### Completeness of Success Criteria

- [ ] CHK054 - Are success criteria defined for all 4 user stories? [Completeness, SC items map to US1-US4]
- [ ] CHK055 - Are success criteria defined for Terraform code quality (fmt, validate, tfsec)? [Completeness, SC-009]
- [ ] CHK056 - Are success criteria defined for security controls (no public IP, logs flowing)? [Completeness, SC-006, SC-007, SC-008]

---

## Scenario Coverage

### Primary Scenario Validation

- [ ] CHK057 - Are requirements defined for initial Terraform deployment (terraform apply)? [Coverage, Primary Flow]
- [ ] CHK058 - Are requirements defined for RDP access via Bastion post-deployment? [Coverage, US2]
- [ ] CHK059 - Are requirements defined for file share access via private endpoint? [Coverage, US3]
- [ ] CHK060 - Are requirements defined for internet access via NAT Gateway? [Coverage, US4]

### Alternate Scenario Coverage

- [ ] CHK061 - Are requirements defined for Terraform state backend configuration? [Coverage, Alternate Flow]
- [ ] CHK062 - Are requirements defined for variable customization via tfvars? [Coverage, FR-021]
- [ ] CHK063 - Are requirements defined for manual post-deployment file share mounting? [Coverage, Clarifications]

### Exception/Error Scenario Coverage

- [ ] CHK064 - Are requirements defined for handling VM naming exceeding 15 chars? [Coverage, Edge Cases]
- [ ] CHK065 - Are requirements defined for storage account name conflicts? [Coverage, Edge Cases]
- [ ] CHK066 - Are requirements defined for CIDR allocation failures? [Coverage, Edge Cases]
- [ ] CHK067 - Are requirements defined for partial deployment failures? [Coverage, Clarifications - incremental redeployment]
- [ ] CHK068 - Are requirements defined for Key Vault secret name conflicts? [Coverage, Edge Cases]

### Recovery Scenario Coverage

- [ ] CHK069 - Are requirements defined for redeploying after fixing errors? [Coverage, Clarifications - keep resources, fix, redeploy]
- [ ] CHK070 - Is the "no rollback/delete" approach clearly specified for failed deployments? [Coverage, Clarifications]

---

## Edge Case Coverage

### Boundary Conditions

- [ ] CHK071 - Is the 15-character NetBIOSlimit explicitly tested in edge cases? [Edge Case, IC-010, Edge Cases section]
- [ ] CHK072 - Are availability zone unavailability scenarios addressed? [Edge Case, Edge Cases section]
- [ ] CHK073 - Are VNet address space exhaustion scenarios addressed? [Edge Case, Edge Cases section]
- [ ] CHK074 - Are global naming conflicts (Key Vault, Storage Account) addressed? [Edge Case, Edge Cases section]

### Configuration Edge Cases

- [ ] CHK075 - Are NSG rule conflict scenarios addressed? [Edge Case, Edge Cases section]
- [ ] CHK076 - Are private DNS resolution failure scenarios addressed? [Edge Case, Edge Cases section]
- [ ] CHK077 - Are disk attachment failure scenarios addressed? [Edge Case, Edge Cases section]

---

## Non-Functional Requirements

### Performance Requirements

- [ ] CHK078 - Are deployment time expectations quantified (30 minutes in SC-001)? [NFR, SC-001]
- [ ] CHK079 - Are connection time expectations quantified (2 minutes RDP in SC-002)? [NFR, SC-002]
- [ ] CHK080 - Are log ingestion timeframes quantified (15 minutes in SC-007)? [NFR, SC-007]
- [ ] CHK081 - Is "Standard HDD is adequate" assumption documented? [NFR, Assumption A-009]

### Cost Requirements

- [ ] CHK082 - Is the cost constraint quantified (<$200/month)? [NFR, SC-013]
- [ ] CHK083 - Are cost optimization requirements specified (HDD vs SSD)? [NFR, IC-007]

### Security Requirements (Non-Functional)

- [ ] CHK084 - Are all security requirements explicitly listed in SEC section? [NFR, SEC-001 through SEC-014]
- [ ] CHK085 - Are encryption requirements specified (at-rest with Microsoft keys)? [NFR, SEC-013]
- [ ] CHK086 - Are soft-delete and purge protection requirements specified? [NFR, SEC-012]

### Compliance Requirements

- [ ] CHK087 - Are log retention requirements specified (180 days)? [NFR, Clarifications]
- [ ] CHK088 - Are resource lock requirements specified for compliance-critical resources? [NFR, SEC-011]

---

## Dependencies & Assumptions

### External Dependencies

- [ ] CHK089 - Are all Terraform/Azure CLI version dependencies documented? [Dependency, D-001, D-002]
- [ ] CHK090 - Is the pre-existing state backend dependency documented? [Dependency, D-005, A-002]
- [ ] CHK091 - Are AVM module registry dependencies documented? [Dependency, D-003]
- [ ] CHK092 - Are deployment permission dependencies documented? [Dependency, A-003]

### Assumption Validation

- [ ] CHK093 - Are quota assumptions documented (VM size, Bastion, NAT Gateway)? [Assumption, A-001]
- [ ] CHK094 - Are availability zone support assumptions documented? [Assumption, A-005]
- [ ] CHK095 - Are naming conflict assumptions documented? [Assumption, A-004]
- [ ] CHK096 - Is the "no domain join" assumption explicitly stated? [Assumption, A-014]
- [ ] CHK097 - Is the "no ExpressRoute/VPN" assumption explicitly stated? [Assumption, A-013]

### Validated Constraints

- [ ] CHK098 - Are all infrastructure constraints (IC-001 through IC-010) fully documented? [Dependency, Infrastructure Constraints section]
- [ ] CHK099 - Are Terraform provider version constraints specified? [Dependency, Plan Technical Context]

---

## Ambiguities & Conflicts

### Potential Ambiguities

- [ ] CHK100 - Is "Bastion SKU (Basic or Standard)" resolved to specific choice? [Ambiguity, Data Model section]
- [ ] CHK101 - Is "RBAC vs Access Policies" for Key Vault resolved to specific choice? [Ambiguity, Data Model section]
- [ ] CHK102 - Is "Action Group notification target" specified beyond "email or webhook"? [Ambiguity, Assumption A-011]
- [ ] CHK103 - Are AVM module version selection criteria specified (always latest stable)? [Ambiguity, Plan Technical Context note]

### Potential Conflicts

- [ ] CHK104 - Do VNet subnet CIDRs in spec match Clarifications section? [Conflict Check, IC-008 vs Clarifications]
- [ ] CHK105 - Does "no backup" decision conflict with any compliance requirements? [Conflict Check, Clarifications vs OS-006]
- [ ] CHK106 - Does "Standard HDD" choice conflict with performance expectations? [Conflict Check, IC-007, A-009]

### Specification Gaps

- [ ] CHK107 - Are requirements missing for NSG flow log configuration? [Gap, SEC-014 mentions flow logs]
- [ ] CHK108 - Are requirements missing for custom DNS configuration? [Gap, Private endpoint DNS]
- [ ] CHK109 - Are requirements missing for alert action group email address? [Gap, Clarifications mention portal only]
- [ ] CHK110 - Are requirements missing for VM OS disk size specification? [Gap, FR-002 mentions standard HDD but not size]

---

## Traceability & Documentation

### Requirement Traceability

- [ ] CHK111 - Are all functional requirements (FR-001 through FR-025) traceable to user stories? [Traceability]
- [ ] CHK112 - Are all security requirements (SEC-001 through SEC-014) traceable to constitution? [Traceability]
- [ ] CHK113 - Are all infrastructure constraints (IC-001 through IC-010) traceable to constitution or clarifications? [Traceability]
- [ ] CHK114 - Are all success criteria (SC-001 through SC-013) traceable to user stories? [Traceability]

### Clarification Documentation

- [ ] CHK115 - Are all clarification session Q&A pairs documented with decisions? [Documentation, Clarifications section]
- [ ] CHK116 - Are clarification decisions integrated into requirements (not just listed)? [Documentation, Requirements reflect clarifications]
- [ ] CHK117 - Are out-of-scope items explicitly documented? [Documentation, Out of Scope section]

### Plan-Spec Alignment

- [ ] CHK118 - Does the plan reference all 25 functional requirements? [Traceability, Plan addresses all FRs]
- [ ] CHK119 - Does the plan reference all 14 security requirements? [Traceability, Plan addresses all SECs]
- [ ] CHK120 - Does the plan reference all 10 infrastructure constraints? [Traceability, Plan addresses all ICs]

---

## Summary

**Total Checklist Items**: 120
**Expected Completion Time**: 2-3 hours for comprehensive review
**Target Audience**: Implementation team preparing to build Terraform code

**Usage Instructions**:
1. Review each checklist item sequentially
2. Mark βœ… for satisfied requirements, ❌ for gaps/issues
3. Document findings in adjacent notes column (if needed)
4. Escalate any ❌ items to spec author for clarification/resolution
5. Re-validate after spec updates

**Pass Criteria**:
- β‰₯95% items marked βœ… (114+ passing items)
- Zero CRITICAL gaps (ambiguities in security, naming, or Terraform structure)
- All conflicts resolved before implementation begins

**Next Steps After Completion**:
- If β‰₯95% pass: Proceed to implementation (terraform code generation)
- If <95% pass: Update spec.md to address gaps, then re-run checklist
- Archive this checklist with implementation for audit trail
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your new checklist, with a comment of something like Checklist prepared.

6. Tasks

Spec Kit uses /speckit.tasks to generate the tasks.md file. The prompt doesn’t require any specific inputs as it analyzes the existing plan to break it down into actionable tasks. Tasks can be evolved through iterating over the tasks.md file by either manually editing it or repeatedly fine tuning the prompt used with /speckit.tasks, or leveraging /speckit.analyze to review/validate and challenge the tasks.

Info

To learn more about what the tasks should include, see the Tasks chapter in the Spec Kit article.

  1. Run the following prompt to generate tasks for our example:
/speckit.tasks
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see something like this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Tasks: Legacy VM Workload Infrastructure

**Feature**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md)
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
**Date**: 2026-01-27
**Branch**: `001-legacy-vm-workload`

**Organization**: Tasks are grouped by user story (US1-US5) to enable independent implementation and testing.

## Format: `[ID] [P?] [Story?] Description`

- **[P]**: Parallelizable (different files, no blocking dependencies)
- **[Story]**: User story label (US1, US2, US3, US4, US5)
- File paths included in descriptions

---

## Phase 1: Setup (Project Initialization)

**Purpose**: Basic project structure and Bicep configuration

- [ ] T001 Create infrastructure directory structure: infra/, infra/docs/
- [ ] T002 Create bicepconfig.json with AVM analyzer rules at infra/bicepconfig.json
- [ ] T003 [P] Create .gitignore file to exclude .bicep build artifacts (*.json from main.bicep compilation)
- [ ] T004 [P] Create project README.md at repository root with quickstart reference
- [ ] T005 Initialize main.bicep with metadata, targetScope='resourceGroup', location parameter

---

## Phase 2: Foundational (Blocking Prerequisites for All User Stories)

**Purpose**: Shared infrastructure that MUST be complete before any user story implementation

**⚠️ CRITICAL**: No user story work can begin until this phase is complete

- [ ] T006 Add parameters to main.bicep: vmSize (default: Standard_D2s_v3), vmAdminUsername (default: vmadmin), vmAdminPasswordSecretName (default: 'vm-admin-password'), availabilityZone (default: 1), fileShareQuotaGiB (default: 1024), logAnalyticsRetentionDays (default: 30)
- [ ] T007 Define variables in main.bicep: suffix = uniqueString(resourceGroup().id), vmPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name, utcNow('u'))}' (NOTE: utcNow() makes deployment non-idempotent - password regenerates on each deploy. Acceptable for initial deployment; consider removing utcNow() for idempotent redeployments)
- [ ] T008 Define resource naming variables: vnetName, vmName, kvName, lawName, stName (storage: no hyphens, max 24 chars)
- [ ] T009 [P] Define tags variable: workload='legacy-vm', environment='production', compliance='legacy-retention', managedBy='bicep-avm'
- [ ] T010 Add AVM module for Log Analytics Workspace (avm/res/operational-insights/workspace:0.15.0) at infra/main.bicep
- [ ] T011 Configure Log Analytics parameters: name, location, retentionInDays, tags
- [ ] T012 Create main.bicepparam file at infra/main.bicepparam with 'using' directive and parameter defaults

**Checkpoint**: Foundation ready - user story phases can now proceed in parallel (if staffed) or sequentially by priority

---

## Phase 3: User Story 1 - Core VM Infrastructure (Priority: P1) 🎯 MVP

**Goal**: Deploy VNet, VM with Windows Server 2016, basic networking - foundational workload infrastructure

**Independent Test**: Deploy to test resource group, verify VM created with correct specs (Standard_D2s_v3, Windows Server 2016), VM communicates within VNet

### Validation for User Story 1 (MANDATORY - Constitution Principle III) ⚠️

> **NOTE: These validation tasks must be executed BEFORE deployment**

- [ ] T013 [US1] Run `bicep build infra/main.bicep` to compile and check syntax errors
- [ ] T014 [US1] Run `az deployment group validate --resource-group rg-legacyvm-test --template-file main.bicep --parameters main.bicepparam`
- [ ] T015 [US1] Run `az deployment group what-if --resource-group rg-legacyvm-test --template-file main.bicep --parameters main.bicepparam`
- [ ] T016 [US1] Review what-if output: verify VNet, VM, NIC will be created with no unexpected changes

### Implementation for User Story 1

- [ ] T017 [P] [US1] Add AVM module for Virtual Network (avm/res/network/virtual-network:0.7.2) in main.bicep
- [ ] T018 [US1] Configure VNet parameters: name, location, addressPrefixes=['10.0.0.0/24'], subnets array with 3 subnets (VM: 10.0.0.0/27, Bastion: 10.0.0.64/26, PE: 10.0.0.128/27)
- [ ] T019 [US1] Add diagnostic settings to VNet module: send to Log Analytics workspace ID reference
- [ ] T020 [P] [US1] Add AVM module for Virtual Machine (avm/res/compute/virtual-machine:0.21.0) in main.bicep
- [ ] T021 [US1] Configure VM parameters: name='vm-legacyvm-${suffix}', computerName='vm-${substring(suffix,0,10)}' (≀15 chars), size=vmSize parameter, adminUsername=vmAdminUsername, adminPassword=kvSecretReference, zone=availabilityZone
- [ ] T022 [US1] Configure VM OS: imageReference for Windows Server 2016, osDisk with Standard_LRS SKU (HDD performance tier)
- [ ] T023 [US1] Configure VM managed identity: type='SystemAssigned'
- [ ] T024 [US1] Configure VM NIC: attach to VM subnet, no public IP, dynamic private IP
- [ ] T025 [US1] Add VM diagnostic settings to Log Analytics workspace
- [ ] T026 [US1] Add VM outputs: vmName, vmResourceId, vmPrivateIP

### Deployment for User Story 1

- [ ] T027 [US1] Create Azure resource group: `az group create --name rg-legacyvm-test --location westus3`
- [ ] T028 [US1] Deploy to test resource group: `az deployment group create --resource-group rg-legacyvm-test --template-file main.bicep --parameters main.bicepparam`
- [ ] T029 [US1] Verify VNet created with 3 subnets in Azure Portal (10.0.0.0/24 address space)
- [ ] T030 [US1] Verify VM created with Windows Server 2016, Standard_D2s_v3, correct zone
- [ ] T031 [US1] Verify VM computer name is ≀15 characters (NetBIOS limit)
- [ ] T032 [US1] Verify diagnostic logs flowing to Log Analytics workspace within 5 minutes

**Checkpoint**: User Story 1 complete - VM infrastructure deployed and validated. Ready to proceed with US2 and US3 in parallel.

---

## Phase 4: User Story 2 - Secure Storage and Data Disk (Priority: P2)

**Goal**: Attach 500GB data disk to VM, deploy storage account with 1TB file share via private endpoint

**Independent Test**: Verify 500GB HDD data disk attached to VM, file share accessible from VM through private endpoint (no internet traversal)

### Implementation for User Story 2

- [ ] T033 [P] [US2] Add data disk to VM module configuration in main.bicep: dataDisks array with disk size=500, sku=Standard_LRS, lun=0, name='datadisk-01'
- [ ] T034 [P] [US2] Add AVM module for Storage Account (avm/res/storage/storage-account:0.31.0) in main.bicep
- [ ] T035 [US2] Configure storage parameters: name='st${replace(suffix, '-', '')}' (max 24 chars, no hyphens), kind='StorageV2', sku='Standard_LRS', accessTier='Hot', publicNetworkAccess='Disabled'
- [ ] T036 [US2] Configure file share in storage module: fileServices with share name='fileshare', quota=fileShareQuotaGiB (1024 GiB)
- [ ] T037 [US2] Add diagnostic settings to storage module: send to Log Analytics workspace
- [ ] T038 [P] [US2] Add AVM module for Private DNS Zone (avm/res/network/private-dns-zone:0.8.0) in main.bicep
- [ ] T039 [US2] Configure DNS zone name: 'privatelink.file.core.windows.net', VNet link to main VNet
- [ ] T040 [P] [US2] Add AVM module for Private Endpoint (avm/res/network/private-endpoint:0.11.1) in main.bicep
- [ ] T041 [US2] Configure private endpoint: subnet=PE subnet, groupIds=['file'], privateDnsZoneResourceIds=[DNS zone ID], link to storage account resource
- [ ] T042 [US2] Add storage outputs: storageAccountName, fileShareName, privateEndpointIP

### Deployment for User Story 2

- [ ] T043 [US2] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T044 [US2] Deploy updated template to test resource group
- [ ] T045 [US2] Verify 500GB data disk attached to VM in Azure Portal (LUN 0, Standard_LRS)
- [ ] T046 [US2] Verify storage account created with public access disabled
- [ ] T047 [US2] Verify file share created with 1024 GiB quota
- [ ] T048 [US2] Verify private endpoint resolves to internal IP (10.0.0.128/27 range): `nslookup st{random}.file.core.windows.net` from VM
- [ ] T049 [US2] Test file share access from VM via Bastion: `Test-NetConnection -ComputerName st{random}.file.core.windows.net -Port 445`

**Checkpoint**: User Stories 1 AND 2 complete - VM with data disk and storage file share via private endpoint validated.

---

## Phase 5: User Story 3 - Secure Access and Secrets Management (Priority: P2)

**Goal**: Deploy Azure Bastion for secure RDP access, Key Vault for storing VM password

**Independent Test**: Connect to VM through Bastion host using password retrieved from Key Vault (no public IP on VM)

### Implementation for User Story 3

- [ ] T050 [P] [US3] Add AVM module for Key Vault (avm/res/key-vault/vault:0.13.3) in main.bicep
- [ ] T051 [US3] Configure Key Vault parameters: name='kv-legacyvm-${suffix}', sku='standard', enableRbacAuthorization=true, softDeleteRetentionInDays=90
- [ ] T052 [US3] Add Key Vault secret via module's secrets parameter: name=vmAdminPasswordSecretName parameter, value=vmPassword variable, contentType='text/plain'
- [ ] T053 [US3] Add RBAC role assignment in Key Vault module: principalId=VM managed identity, roleDefinitionIdOrName='Key Vault Secrets User'
- [ ] T054 [US3] Add Key Vault diagnostic settings to Log Analytics workspace
- [ ] T055 [P] [US3] Update VM module configuration: change adminPassword to reference Key Vault secret (use getSecret() or secretReference)
- [ ] T056 [P] [US3] Add AVM module for Bastion Host (avm/res/network/bastion-host:0.8.2) in main.bicep
- [ ] T057 [US3] Configure Bastion parameters: name='bas-legacyvm-${suffix}', sku='Basic', vnetId=VNet resource ID, subnetName='AzureBastionSubnet'
- [ ] T058 [US3] Add Bastion diagnostic settings to Log Analytics workspace
- [ ] T059 [US3] Add Key Vault and Bastion outputs: kvName, kvResourceId, bastionName, bastionResourceId

### Deployment for User Story 3

- [ ] T060 [US3] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T061 [US3] Deploy updated template to test resource group (expected duration: 15-20 minutes for Bastion)
- [ ] T062 [US3] Verify Key Vault created with RBAC enabled (not access policies)
- [ ] T063 [US3] Verify VM password stored as Key Vault secret: `az keyvault secret show --name vm-admin-password --vault-name kv-legacyvm-{suffix}`
- [ ] T064 [US3] Verify VM managed identity has 'Key Vault Secrets User' role on Key Vault
- [ ] T065 [US3] Verify Azure Bastion deployed successfully in Bastion subnet
- [ ] T066 [US3] Test Bastion connectivity: Connect to VM via Azure Portal β†’ VM β†’ Connect β†’ Bastion, use username='vmadmin' and password from Key Vault
- [ ] T067 [US3] Verify VM has no public IP address assigned (confirm access only through Bastion)

**Checkpoint**: User Stories 1, 2, AND 3 complete - Full VM infrastructure with secure access and storage operational.

---

## Phase 6: User Story 4 - Internet Connectivity and Network Security (Priority: P3)

**Goal**: Configure NAT Gateway for outbound internet, implement NSGs for all subnets with least-privilege rules

**Independent Test**: Verify VM reaches internet through NAT Gateway, NSG rules block unauthorized traffic

### Implementation for User Story 4

- [ ] T068 [P] [US4] Add AVM module for NAT Gateway (avm/res/network/nat-gateway:2.0.1) in main.bicep
- [ ] T069 [US4] Configure NAT Gateway parameters: name='nat-legacyvm-${suffix}', zone=availabilityZone, publicIpAddressObjects=[{name: 'pip-nat'}]
- [ ] T070 [US4] Update VNet module configuration: associate NAT Gateway with VM subnet (natGatewayId in subnet definition)
- [ ] T071 [US4] Add NAT Gateway diagnostic settings to Log Analytics workspace
- [ ] T072 [P] [US4] Add AVM module for VM Subnet NSG (avm/res/network/network-security-group:0.5.2) in main.bicep
- [ ] T073 [US4] Configure VM NSG security rules: **CRITICAL - inbound allow TCP 3389 from Bastion subnet (10.0.0.64/26) priority 100**, inbound deny all (priority 4096), outbound allow Internet (priority 100), outbound allow VNet (priority 200), outbound deny all (priority 4096)
- [ ] T074 [US4] Update VNet module: associate VM NSG with VM subnet (networkSecurityGroupId in subnet definition)
- [ ] T075 [US4] Add VM NSG diagnostic settings to Log Analytics workspace
- [ ] T076 [P] [US4] Add AVM module for Bastion Subnet NSG (avm/res/network/network-security-group:0.5.2) in main.bicep
- [ ] T077 [US4] Configure Bastion NSG security rules: inbound allow 443 from Internet, allow GatewayManager 443, allow AzureLoadBalancer 443, allow Bastion communication 8080/5701; outbound allow SSH/RDP to VNet, allow Azure Cloud 443, allow Bastion communication, allow HTTP 80
- [ ] T078 [US4] Update VNet module: associate Bastion NSG with Bastion subnet
- [ ] T079 [US4] Add Bastion NSG diagnostic settings to Log Analytics workspace
- [ ] T080 [P] [US4] Add AVM module for PE Subnet NSG (avm/res/network/network-security-group:0.5.2) in main.bicep
- [ ] T081 [US4] Configure PE NSG security rules: inbound allow TCP 445 from VM subnet (10.0.0.0/27), inbound deny all; outbound allow all
- [ ] T082 [US4] Update VNet module: associate PE NSG with PE subnet
- [ ] T083 [US4] Add PE NSG diagnostic settings to Log Analytics workspace
- [ ] T084 [US4] Add NSG and NAT Gateway outputs: nsgVmName, nsgBastionName, nsgPeName, natGatewayName

### Deployment for User Story 4

- [ ] T085 [US4] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T086 [US4] Deploy updated template to test resource group
- [ ] T087 [US4] Verify NAT Gateway created with public IP and associated with VM subnet
- [ ] T088 [US4] Verify 3 NSGs created and associated with correct subnets
- [ ] T089 [US4] Test outbound internet from VM via Bastion RDP session: `Test-NetConnection -ComputerName google.com -Port 443` (should succeed through NAT Gateway)
- [ ] T090 [US4] Test NSG deny rules: attempt unauthorized inbound connection to VM (should be blocked)
- [ ] T091 [US4] Verify all NSG diagnostic logs flowing to Log Analytics workspace

**Checkpoint**: User Stories 1-4 complete - Full network security and internet connectivity operational.

---

## Phase 7: User Story 5 - Monitoring and Alerting (Priority: P3)

**Goal**: Configure diagnostic settings for all resources, deploy 3 critical alerts (VM stopped, disk >85%, Key Vault access failures)

**Independent Test**: Verify diagnostic logs flowing to Log Analytics, trigger test alert scenarios and confirm alerts fire

### Implementation for User Story 5

- [ ] T092 [P] [US5] Add AVM module for VM Stopped Alert (avm/res/insights/metric-alert:0.4.1) in main.bicep
- [ ] T093 [US5] Configure VM stopped alert: name='alert-vm-stopped-legacyvm-${suffix}', targetResourceId=VM resource ID, metricName='Percentage CPU', operator='LessThan', threshold=1, aggregation='Average', windowSize='PT15M', severity=0 (Critical), enabled=true, autoMitigate=false
- [ ] T094 [US5] Ensure no action groups configured (Portal-only notifications per MON-003)
- [ ] T095 [P] [US5] Add AVM module for Disk Space Alert (avm/res/insights/metric-alert:0.4.1) in main.bicep
- [ ] T096 [US5] Configure disk space alert: name='alert-disk-space-legacyvm-${suffix}', targetResourceId=VM resource ID, metricName='OS Disk Used Percentage', operator='GreaterThan', threshold=85, aggregation='Average', windowSize='PT5M', severity=0, enabled=true, autoMitigate=false
- [ ] T097 [P] [US5] Add AVM module for Key Vault Access Failure Alert (avm/res/insights/metric-alert:0.4.1) in main.bicep
- [ ] T098 [US5] Configure KV alert: name='alert-kv-access-fail-legacyvm-${suffix}', targetResourceId=Key Vault resource ID, metricName='ServiceApiHit', dimensions=[{name: 'ActivityName', operator: 'Include', values: ['SecretGet']}, {name: 'StatusCode', operator: 'Include', values: ['Unauthorized']}], operator='GreaterThan', threshold=0, aggregation='Count', windowSize='PT5M', severity=0
- [ ] T099 [US5] Review all existing resource modules: verify diagnostic settings already configured for VNet, VM, Storage, Key Vault, NSGs, NAT Gateway, Bastion (completed in previous phases)
- [ ] T100 [US5] Add alert outputs: alertVmStoppedName, alertDiskSpaceName, alertKvFailureName

### Deployment for User Story 5

- [ ] T101 [US5] Re-run validation: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T102 [US5] Deploy updated template to test resource group
- [ ] T103 [US5] Verify Log Analytics workspace contains logs from all resources: run query in Azure Portal β†’ Log Analytics β†’ Logs β†’ `AzureDiagnostics | where ResourceGroup == 'rg-legacyvm-test' | summarize count() by ResourceType`
- [ ] T104 [US5] Verify 3 metric alerts created and enabled in Azure Portal β†’ Monitor β†’ Alerts
- [ ] T105 [US5] Test VM stopped alert: Stop VM, wait 15 minutes, verify alert fires and visible in Portal
- [ ] T106 [US5] Test Key Vault access failure alert: Attempt to access non-existent secret `az keyvault secret show --name fake-secret --vault-name kv-legacyvm-{suffix}`, wait 5 minutes, verify alert fires
- [ ] T107 [US5] Verify alert notifications visible in Azure Portal β†’ Monitor β†’ Alerts (no external action groups configured)
- [ ] T108 [US5] Document alert testing procedures in infra/docs/deployment.md

**Checkpoint**: All 5 user stories complete - Full monitoring and alerting operational. MVP infrastructure complete!

---

## Phase 8: Polish & Cross-Cutting Concerns

**Purpose**: Final improvements and documentation that span multiple user stories

- [ ] T109 [P] Review main.bicep for code quality: verify all resources have comments explaining purpose
- [ ] T110 [P] Review main.bicepparam for documentation: verify parameter descriptions and defaults documented
- [ ] T111 [P] Validate constitution compliance: check all 6 principles satisfied (IC-001 to IC-006, SEC-001 to SEC-008)
- [ ] T112 [P] Run final validation suite: `bicep build`, `az deployment validate`, `what-if` analysis
- [ ] T113 Update infra/docs/deployment.md with full deployment outcomes and lessons learned
- [ ] T114 [P] Create architecture diagram in infra/docs/architecture.md showing all resources and dependencies
- [ ] T115 Verify all success criteria met (SC-001 to SC-011): deployment time <20 minutes, all resources operational, logs flowing, alerts working
- [ ] T116 Execute quickstart.md validation end-to-end: follow deployment guide steps, verify successful deployment
- [ ] T117 [P] Review Azure Security Center recommendations: address any high-severity compliance issues
- [ ] T118 Document estimated monthly costs in README.md: VM ~$70, Storage ~$50, Bastion ~$140, Other ~$10 = Total ~$270/month
- [ ] T119 Create CHANGELOG.md entry: document initial deployment date, version 1.0.0, all resources deployed
- [ ] T120 Final code review: verify no hardcoded values, all parameters in main.bicepparam, rich comments present

**Checkpoint**: Production-ready infrastructure complete. Ready for operational handoff.

---

## Dependencies & Execution Order

### Phase Dependencies

1. **Setup (Phase 1)**: No dependencies - start immediately
2. **Foundational (Phase 2)**: Depends on Setup - BLOCKS all user stories
3. **User Story 1 (Phase 3)**: Depends on Foundational - MVP foundation, MUST complete first
4. **User Story 2 (Phase 4)**: Depends on Foundational - Can proceed after US1 or in parallel
5. **User Story 3 (Phase 5)**: Depends on Foundational and US1 (VM must exist for Key Vault password reference) - Bastion depends on VNet from US1
6. **User Story 4 (Phase 6)**: Depends on Foundational and US1 (NAT Gateway and NSGs associate with VNet subnets from US1)
7. **User Story 5 (Phase 7)**: Depends on all previous user stories (alerts target VM, Key Vault; diagnostic settings reference Log Analytics from Foundational)
8. **Polish (Phase 8)**: Depends on all user stories complete

### User Story Interdependencies

- **US1 (Core VM)**: Independent after Foundational - can start first
- **US2 (Storage)**: Depends on US1 (data disk attaches to VM, private endpoint needs VNet)
- **US3 (Secure Access)**: Depends on US1 (Key Vault stores password for VM, Bastion accesses VM, both need VNet)
- **US4 (Network Security)**: Depends on US1 (NAT Gateway and NSGs associate with VNet subnets)
- **US5 (Monitoring)**: Depends on US1, US3 (alerts target VM and Key Vault resources)

### Recommended Execution Sequence

**Option 1 - Sequential by Priority** (single developer):
1. Setup β†’ Foundational β†’ US1 β†’ US2 β†’ US3 β†’ US4 β†’ US5 β†’ Polish

**Option 2 - Parallel with Blocking** (team of 3):
1. Setup β†’ Foundational
2. US1 (Developer 1)
3. After US1: US2 + US3 + US4 in parallel (Developers 1, 2, 3)
4. After US2/US3/US4: US5 (any developer)
5. Polish

### Validation Cadence

- Run `bicep build` after every file modification
- Run `az deployment validate` before every deployment
- Run `what-if` analysis before every deployment to test/production
- Test each user story independently after its deployment phase

### MVP Definition

**Minimum Viable Product = User Story 1 Complete**

At T032 completion, you have:
- Windows Server 2016 VM operational
- VNet with 3 subnets configured
- Basic infrastructure validated

This is sufficient to demonstrate foundational workload capabilities. Subsequent user stories add storage, secure access, network security, and monitoring incrementally.

---

## Parallel Execution Opportunities

### Tasks That Can Run in Parallel

**Phase 1 (Setup)**: T003, T004 can run in parallel with T002

**Phase 2 (Foundational)**: T009, T012 can run in parallel after T008

**Phase 3 (US1)**: T017, T020 can run in parallel (research different modules)

**Phase 4 (US2)**: T033, T034, T038, T040 can run in parallel (different module additions)

**Phase 5 (US3)**: T050, T055, T056 can run in parallel (Key Vault, Bastion, VM update)

**Phase 6 (US4)**: T068, T072, T076, T080 can run in parallel (NAT Gateway + 3 NSGs)

**Phase 7 (US5)**: T092, T095, T097 can run in parallel (3 independent alert modules)

**Phase 8 (Polish)**: T109, T110, T111, T114, T117, T118 can run in parallel (different file updates)

### Example Parallel Workflow (3 developers)

**Sprint 1 (Week 1)**:
- Dev 1: T001-T012 (Setup + Foundational)
- Dev 2: T003-T004 in parallel with Dev 1
- Dev 3: Start planning US2 tasks

**Sprint 2 (Week 2)**:
- Dev 1: T013-T032 (US1 - Core VM)
- Dev 2 + Dev 3: Prepare for US2/US3 parallel work

**Sprint 3 (Week 3)**:
- Dev 1: T033-T049 (US2 - Storage)
- Dev 2: T050-T067 (US3 - Secure Access)
- Dev 3: T068-T091 (US4 - Network Security)

**Sprint 4 (Week 4)**:
- Dev 1: T092-T108 (US5 - Monitoring)
- Dev 2 + Dev 3: T109-T120 (Polish) in parallel

**Total Duration**: ~4 weeks with 3 developers, or ~6 weeks sequential

---

## Implementation Strategy

### MVP-First Approach

1. **Deliver US1 first** (Phase 3: Core VM Infrastructure)
  - Provides foundational value: operational VM with networking
  - Independent testable increment
  - Validates Bicep template structure and AVM module usage

2. **Add US2 + US3** (Phase 4-5: Storage + Secure Access)
  - Provides secure operations capabilities
  - Data storage layer complete
  - Bastion access operational

3. **Add US4 + US5** (Phase 6-7: Network Security + Monitoring)
  - Production-grade security and observability
  - Complete infrastructure compliance

### Incremental Delivery Value

| Completion Point | Value Delivered | Can Deploy to Production? |
|------------------|-----------------|---------------------------|
| After US1 | Basic VM workload operational | No - missing security controls |
| After US1 + US3 | VM with secure access | No - missing storage and monitoring |
| After US1 + US2 + US3 | VM with storage and secure access | Maybe - basic functionality complete, but no monitoring |
| After US1-US4 | Full network security in place | Maybe - functionally complete, limited observability |
| After US1-US5 | Full monitoring and alerting | **YES** - production-ready |

### Rollback Strategy

- Each user story deployment is incremental - previous state preserved
- ARM deployment mode: Incremental (default) - only adds/updates resources
- Rollback: Redeploy previous version of main.bicep (if maintained in git)
- Nuclear option: Delete resource group and redeploy from scratch (acceptable for this single-RG workload)

---

## Task Validation Checklist

Before marking tasks.md as complete, verify:

- [ ] All 5 user stories from spec.md mapped to task phases
- [ ] Each user story has validation tasks (T013-T016 pattern for Bicep/ARM validation)
- [ ] Each user story has implementation tasks with AVM module references
- [ ] Each user story has deployment tasks with verification steps
- [ ] Each user story is independently testable (checkpoint verifications defined)
- [ ] All 16 functional requirements (FR-001 to FR-016) covered in tasks
- [ ] All 8 security requirements (SEC-001 to SEC-008) covered in tasks
- [ ] All 6 infrastructure constraints (IC-001 to IC-006) covered in tasks
- [ ] All 5 monitoring requirements (MON-001 to MON-005) covered in tasks
- [ ] Task IDs sequential (T001 to T120)
- [ ] Parallel tasks marked with [P]
- [ ] User story tasks marked with [US1] to [US5]
- [ ] File paths included in task descriptions
- [ ] Constitution validation tasks included (T111)
- [ ] Quickstart validation task included (T116)

**Total Tasks**: 120
**Estimated Duration**: 4-6 weeks (depending on team size and parallel execution)
/speckit.tasks
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see something like this:
Specify Bootstrap
alt text alt text

# Implementation Tasks: Legacy Business Application Infrastructure

**Feature**: 001-my-legacy-workload
**Status**: Ready for Implementation
**Created**: 2026-02-18
**Input**: [spec.md](./spec.md) | [plan.md](./plan.md)
**Total Tasks**: 78

**Organization**: Tasks organized by User Story to enable independent implementation and parallel execution. Each phase delivers a complete, independently testable capability.

**Task Format**: `- [ ] [TaskID] [P] [StoryLabel] Description with file path`
- **[P]** = Parallelizable (different files, no dependencies on incomplete tasks)
- **[Story]** = User Story label (US1, US2, US3, US4)

---

## Task Execution Strategy

### Implementation Approach
- **MVP First**: Complete User Story 1 (P1) before moving to other stories
- **Incremental Delivery**: Each user story phase is fully functional and independently testable
- **Parallel Opportunities**: Tasks marked [P] can run concurrently within the same phase
- **Validation Gates**: Terraform validate + plan review between phases

### Dependency Flow
1. **Setup** β†’ **Foundational** (blocking)
2. **Foundational** β†’ **US1** (P1 - Core Compute)
3. **US1** β†’ **US2** (P2 - Secure Access)
4. **US1 + US2** β†’ **US3** (P3 - Storage)
5. **US1 + US2 + US3** β†’ **US4** (P4 - Observability)
6. **All User Stories** β†’ **Polish**

### Independent Test Criteria Per Story
- **US1**: VM running in VNet with NSGs, no external access
- **US2**: RDP via Bastion using Key Vault password
- **US3**: File share accessible from VM via private endpoint
- **US4**: Internet via NAT, logs in Log Analytics, alerts functional

---

## Phase 1: Setup & Project Initialization

**Objective**: Initialize Terraform project structure, install tooling, configure remote state backend

**Prerequisites**: Azure subscription with Contributor access, Azure CLI authenticated, Terraform >= 1.9.0 installed

### Project Structure Tasks

- [ ] T001 Create terraform/ directory at repository root
- [ ] T002 Create docs/ directory for documentation
- [ ] T003 Create .github/workflows/ directory for CI/CD (optional)
- [ ] T004 Create .gitignore file with Terraform patterns (.terraform/, *.tfstate, *.tfplan, *.tfvars except *.tfvars.example)

### Terraform Configuration Files (Shell)

- [ ] T005 [P] Create terraform/terraform.tf with provider version constraints (Terraform >= 1.9.0, azurerm ~> 4.0, random ~> 3.6)
- [ ] T006 [P] Create terraform/variables.tf shell with empty file (will populate in Phase 2)
- [ ] T007 [P] Create terraform/locals.tf shell with empty file (will populate in Phase 2)
- [ ] T008 [P] Create terraform/main.tf shell with header comment
- [ ] T009 [P] Create terraform/outputs.tf shell with empty file (will populate per user story)
- [ ] T010 [P] Create terraform/prod.tfvars.example template file

### State Backend Configuration

- [ ] T011 Verify pre-existing Azure Storage Account for Terraform state exists (per spec dependency D-005)
- [ ] T012 Create terraform/backend.hcl.example with backend configuration template (storage_account_name, container_name, key, resource_group_name)
- [ ] T013 Document backend configuration in terraform/README.md (instructions for creating backend.hcl from example)

### Tooling Installation

- [ ] T014 [P] Install tfsec >= 1.28 for security scanning (per plan Security Tooling)
- [ ] T015 [P] Install checkov >= 3.0 for compliance scanning (per plan Security Tooling)
- [ ] T016 [P] Verify Terraform CLI >= 1.9.0 installed (terraform version)

### Documentation

- [ ] T017 Create terraform/README.md with deployment instructions (init β†’ fmt β†’ validate β†’ plan β†’ apply workflow)
- [ ] T018 Create docs/README.md with project overview
- [ ] T019 Create docs/architecture.md placeholder for infrastructure diagrams

**Phase 1 Validation**:
- βœ… Directory structure created
- βœ… All Terraform shell files exist
- βœ… Backend configuration documented
- βœ… Security tooling installed

---

## Phase 2: Foundational Infrastructure (Blocking Prerequisites)

**Objective**: Deploy foundational resources required by all user stories (Resource Group, naming resources, Log Analytics for diagnostic settings)

**Prerequisites**: Phase 1 complete, backend.hcl configured, Terraform initialized

### Terraform Initialization

- [ ] T020 Run terraform init -backend-config=backend.hcl to initialize backend and download providers

### Random Resources for Naming

- [ ] T021 [US1] Implement random_string resource in terraform/main.tf for unique suffix (6 chars, lowercase alphanumeric)
- [ ] T022 [US1] Implement random_password resource in terraform/main.tf for VM admin password (24 chars, complexity requirements per plan)

### Locals for Naming Convention

- [ ] T023 [US1] Define locals.tf unique_suffix from random_string
- [ ] T024 [US1] Define locals.tf location_abbr = "wus3"
- [ ] T025 [US1] [P] Define locals.tf resource group name (rg-avmlegacy-prod-wus3)
- [ ] T026 [US1] [P] Define locals.tf common_tags map with Environment, Workload, ManagedBy, Region, Spec

### Core Variables

- [ ] T027 [P] Define variables.tf location variable (default: westus3, validation: must equal westus3)
- [ ] T028 [P] Define variables.tf workload_name variable (default: avmlegacy)
- [ ] T029 [P] Define variables.tf environment variable (default: prod, validation: must equal prod)
- [ ] T030 [P] Define variables.tf tags variable (map of strings)

### Resource Group

- [ ] T031 [US1] Implement azurerm_resource_group in terraform/main.tf (name from locals, location from var.location, tags from locals.common_tags)
- [ ] T032 [US1] Implement azurerm_management_lock for Resource Group in terraform/main.tf (CanNotDelete per spec SEC-011)

### Log Analytics Workspace (Required for Diagnostic Settings)

- [ ] T033 Implement module block for Log Analytics Workspace in terraform/main.tf using Azure/avm-res-operationalinsights-workspace/azurerm
- [ ] T034 Configure Log Analytics SKU = PerGB2018, retention = 180 days (per spec clarifications)
- [ ] T035 Configure Log Analytics lock via AVM module lock interface (CanNotDelete)
- [ ] T036 [P] Define variables.tf log_analytics_retention_days variable (default: 180, validation: must equal 180)
- [ ] T037 Define locals.tf law_name using naming convention (law-avmlegacy-{suffix})
- [ ] T038 [P] Implement terraform/outputs.tf log_analytics_workspace_id output
- [ ] T039 [P] Implement terraform/outputs.tf log_analytics_workspace_name output

### Foundational Validation

- [ ] T040 Populate terraform/prod.tfvars with foundational variable values (location, workload_name, environment, tags)
- [ ] T041 Run terraform fmt -recursive to format all .tf files
- [ ] T042 Run terraform validate to check syntax
- [ ] T043 Run terraform plan -var-file=prod.tfvars to preview foundational resources (expect: Resource Group + Lock + Random resources + Log Analytics)
- [ ] T044 Review plan output for correctness (naming, tags, lock, retention)
- [ ] T045 Run terraform apply plan.tfplan to deploy foundational infrastructure
- [ ] T046 Verify Resource Group, random resources, and Log Analytics created in Azure Portal

**Phase 2 Validation**:
- βœ… Resource Group deployed to westus3
- βœ… Resource Group lock (CanNotDelete) applied
- βœ… Random resources generated (suffix, password)
- βœ… Log Analytics Workspace deployed with 180-day retention
- βœ… Terraform state stored in remote backend

---

## Phase 3: User Story 1 (P1) - Core Compute and Network Infrastructure

**Objective**: Deploy Windows Server 2016 VM within isolated VNet with proper subnet segmentation and network security controls

**User Story**: Deploy a Windows Server 2016 virtual machine within an isolated virtual network with proper subnet segmentation and network security controls.

**Independent Test**: Verify VM is created and running in specified VNet with proper subnets. Confirm NSGs attached to subnets and default deny rules in place. VM should be isolated with no internet or external access.

**Prerequisites**: Phase 2 (Foundational) complete - Resource Group and Log Analytics exist

### Variables for Networking and VM

- [ ] T047 [P] [US1] Define variables.tf vnet_address_space variable (default: ["10.0.0.0/24"], validation: must equal 10.0.0.0/24)
- [ ] T048 [P] [US1] Define variables.tf vm_subnet_cidr variable (default: 10.0.0.0/27)
- [ ] T049 [P] [US1] Define variables.tf bastion_subnet_cidr variable (default: 10.0.0.32/26)
- [ ] T050 [P] [US1] Define variables.tf private_endpoint_subnet_cidr variable (default: 10.0.0.96/28)
- [ ] T051 [P] [US1] Define variables.tf vm_size variable (default: Standard_D2s_v3)
- [ ] T052 [P] [US1] Define variables.tf vm_admin_username variable (default: vmadmin, validation: must equal vmadmin)
- [ ] T053 [P] [US1] Define variables.tf vm_data_disk_size_gb variable (default: 500, validation: must equal 500)
- [ ] T054 [P] [US1] Define variables.tf availability_zone variable (default: 1, validation: must be 1, 2, or 3)

### Locals for Resource Naming

- [ ] T055 [US1] Define locals.tf vnet_name using naming convention (vnet-avmlegacy-{suffix})
- [ ] T056 [US1] [P] Define locals.tf vm_nsg_name (nsg-vm-avmlegacy-{suffix})
- [ ] T057 [US1] [P] Define locals.tf bastion_nsg_name (nsg-bastion-avmlegacy-{suffix})
- [ ] T058 [US1] [P] Define locals.tf private_endpoint_nsg_name (nsg-pe-avmlegacy-{suffix})
- [ ] T059 [US1] Define locals.tf vm_name_raw and vm_name (vm-avmlegacy-{suffix}, truncated to 15 chars for NetBIOS per spec FR-004)
- [ ] T060 [US1] Define locals.tf vm_computer_name = vm_name (same as VM name, ensures 15 char limit)
- [ ] T061 [US1] [P] Define locals.tf vm_subnet_name, bastion_subnet_name (AzureBastionSubnet exact), private_endpoint_subnet_name

### Virtual Network

- [ ] T062 [US1] Implement module block for VNet in terraform/main.tf using Azure/avm-res-network-virtualnetwork/azurerm
- [ ] T063 [US1] Configure VNet address_space = var.vnet_address_space (10.0.0.0/24)
- [ ] T064 [US1] Configure VNet subnets map with 3 subnets (vm_subnet, bastion_subnet, private_endpoint_subnet with CIDRs from variables)
- [ ] T065 [US1] Configure private_endpoint subnet with private_endpoint_network_policies_enabled = false (per spec IC-009)
- [ ] T066 [US1] Configure VNet diagnostic_settings sending logs to Log Analytics (reference module.log_analytics.resource_id)
- [ ] T067 [US1] Configure VNet lock via AVM module lock interface (CanNotDelete)
- [ ] T068 [US1] [P] Implement terraform/outputs.tf virtual_network_name output
- [ ] T069 [US1] [P] Implement terraform/outputs.tf virtual_network_id output

### Network Security Groups

- [ ] T070 [P] [US1] Implement module block for VM NSG in terraform/main.tf using Azure/avm-res-network-networksecuritygroup/azurerm
- [ ] T071 [US1] Configure VM NSG security_rules: Allow-RDP-From-Bastion (priority 100, source: bastion_subnet_cidr, dest: vm_subnet_cidr, port 3389, protocol TCP)
- [ ] T072 [US1] Configure VM NSG security_rules: Deny-All-Inbound (priority 4096, deny all per spec SEC-004)
- [ ] T073 [US1] Configure VM NSG diagnostic_settings sending logs to Log Analytics
- [ ] T074 [P] [US1] Implement module block for Bastion NSG in terraform/main.tf using Azure/avm-res-network-networksecuritygroup/azurerm
- [ ] T075 [US1] Configure Bastion NSG security_rules: Allow-HTTPS-Inbound (priority 100, source: Internet, port 443 per Azure Bastion requirement)
- [ ] T076 [US1] Configure Bastion NSG security_rules: Allow-GatewayManager-Inbound (priority 110, source: GatewayManager service tag, port 443)
- [ ] T077 [US1] Configure Bastion NSG security_rules: Allow-RDP-To-VM-Subnet (outbound, priority 100, dest: vm_subnet_cidr, port 3389)
- [ ] T078 [US1] Configure Bastion NSG security_rules: Allow-AzureCloud-Outbound (outbound, priority 110, dest: AzureCloud service tag, port 443)
- [ ] T079 [US1] Configure Bastion NSG diagnostic_settings sending logs to Log Analytics
- [ ] T080 [P] [US1] Implement module block for Private Endpoint NSG in terraform/main.tf using Azure/avm-res-network-networksecuritygroup/azurerm
- [ ] T081 [US1] Configure Private Endpoint NSG security_rules: Allow-SMB-From-VM-Subnet (priority 100, source: vm_subnet_cidr, port 445 per spec SEC-007)
- [ ] T082 [US1] Configure Private Endpoint NSG security_rules: Deny-All-Inbound (priority 4096, deny all)
- [ ] T083 [US1] Configure Private Endpoint NSG diagnostic_settings sending logs to Log Analytics

### NSG-Subnet Associations

- [ ] T084 [P] [US1] Implement azurerm_subnet_network_security_group_association for vm_subnet in terraform/main.tf
- [ ] T085 [P] [US1] Implement azurerm_subnet_network_security_group_association for bastion_subnet in terraform/main.tf
- [ ] T086 [P] [US1] Implement azurerm_subnet_network_security_group_association for private_endpoint_subnet in terraform/main.tf

### Virtual Machine

- [ ] T087 [US1] Implement module block for VM in terraform/main.tf using Azure/avm-res-compute-virtualmachine/azurerm
- [ ] T088 [US1] Configure VM name = locals.vm_name (truncated to 15 chars)
- [ ] T089 [US1] Configure VM computer_name = locals.vm_computer_name (same as name, ≀15 chars per spec FR-004)
- [ ] T090 [US1] Configure VM vm_size = var.vm_size (Standard_D2s_v3)
- [ ] T091 [US1] Configure VM zone = var.availability_zone (1, 2, or 3 per spec FR-023)
- [ ] T092 [US1] Configure VM source_image_reference (publisher: MicrosoftWindowsServer, offer: WindowsServer, sku: 2016-Datacenter, version: latest)
- [ ] T093 [US1] Configure VM os_profile with admin_username = var.vm_admin_username (vmadmin)
- [ ] T094 [US1] Configure VM os_profile admin_password referencing Key Vault secret (will update in Phase 4 after Key Vault deployed - use placeholder for now)
- [ ] T095 [US1] Configure VM network_interfaces with nic1 connected to vm_subnet (subnet_id from module.virtual_network.subnets)
- [ ] T096 [US1] Configure VM network_interfaces with private_ip_address_allocation = Dynamic, public_ip_address_id = null (no public IP per spec FR-011)
- [ ] T097 [US1] Configure VM os_disk (name: {vm_name}-osdisk, caching: ReadWrite, storage_account_type: Standard_LRS per spec FR-002)
- [ ] T098 [US1] Configure VM data_disks with data1 (name: {vm_name}-datadisk, lun: 0, caching: ReadWrite, storage_account_type: Standard_LRS, disk_size_gb: 500 per spec FR-003)
- [ ] T099 [US1] Configure VM managed_identities with system_assigned = true (per spec SEC-001)
- [ ] T100 [US1] Configure VM diagnostic_settings sending logs to Log Analytics
- [ ] T101 [US1] Configure VM lock via AVM module lock interface (CanNotDelete per spec SEC-011)
- [ ] T102 [US1] Add depends_on for Key Vault module (will add in Phase 4)
- [ ] T103 [US1] [P] Implement terraform/outputs.tf vm_name output
- [ ] T104 [US1] [P] Implement terraform/outputs.tf vm_id output
- [ ] T105 [US1] [P] Implement terraform/outputs.tf vm_private_ip_address output
- [ ] T106 [US1] [P] Implement terraform/outputs.tf vm_computer_name output

### User Story 1 Deployment

- [ ] T107 [US1] Populate terraform/prod.tfvars with US1 variable values (vnet_address_space, subnet CIDRs, vm_size, vm_admin_username, availability_zone)
- [ ] T108 [US1] Run terraform fmt -recursive
- [ ] T109 [US1] Run terraform validate
- [ ] T110 [US1] Run terraform plan -var-file=prod.tfvars -out=us1.tfplan (expect: VNet + 3 subnets + 3 NSGs + 3 associations + VM with disks)
- [ ] T111 [US1] Review plan output for US1 completeness (check VM size, computer name ≀15 chars, NSG rules, no public IP)
- [ ] T112 [US1] Run terraform apply us1.tfplan
- [ ] T113 [US1] Verify VM running in Azure Portal (check VM properties: size, computer name, admin username, no public IP)
- [ ] T114 [US1] Verify VNet has 3 subnets with correct CIDR allocations
- [ ] T115 [US1] Verify NSGs attached to each subnet
- [ ] T116 [US1] Verify NSG rules (VM NSG allows RDP from Bastion only, Bastion NSG allows HTTPS from Internet)
- [ ] T117 [US1] Verify VM has no external access (cannot reach internet, not reachable from internet)

**Phase 3 (US1) Validation**:
- βœ… VM running with Standard_D2s_v3, Windows Server 2016, 500GB data disk
- βœ… VNet (10.0.0.0/24) with 3 subnets created
- βœ… NSGs attached to subnets with deny-by-default rules
- βœ… VM isolated (no public IP, no internet access)
- βœ… Computer name ≀15 characters
- βœ… VM admin username = vmadmin

**Parallel Execution Opportunities (US1)**:
- Tasks T047-T054 (variable definitions) can run in parallel
- Tasks T056-T058 (NSG naming locals) can run in parallel
- Tasks T070, T074, T080 (NSG module blocks) can run in parallel after VNet deployed
- Tasks T084-T086 (NSG associations) can run in parallel after NSGs created
- Tasks T103-T106 (output definitions) can run in parallel

---

## Phase 4: User Story 2 (P2) - Secure Remote Access

**Objective**: Enable secure remote access to VM through Azure Bastion and store VM administrator password securely in Azure Key Vault

**User Story**: Enable secure remote access to the virtual machine through Azure Bastion and store the VM administrator password securely in Azure Key Vault.

**Independent Test**: Verify administrators can connect to VM via Azure Bastion using credentials retrieved from Key Vault. Confirm VM has no public IP address.

**Prerequisites**: Phase 3 (US1) complete - VM and VNet deployed

### Variables for Key Vault and Bastion

- [ ] T118 [P] [US2] Define variables.tf vm_admin_secret_name variable (default: vm-admin-password for Key Vault secret name per spec FR-016)

### Locals for Resource Naming

- [ ] T119 [P] [US2] Define locals.tf bastion_name (bastion-avmlegacy-{suffix})
- [ ] T120 [P] [US2] Define locals.tf key_vault_name (kv-avmlegacy-{suffix}, note: 3-24 chars, globally unique)

### Key Vault

- [ ] T121 [US2] Implement module block for Key Vault in terraform/main.tf using Azure/avm-res-keyvault-vault/azurerm
- [ ] T122 [US2] Configure Key Vault name = locals.key_vault_name (3-24 chars)
- [ ] T123 [US2] Configure Key Vault tenant_id from data.azurerm_client_config.current
- [ ] T124 [US2] Configure Key Vault sku_name = standard
- [ ] T125 [US2] Configure Key Vault soft_delete_retention_days = 90, purge_protection_enabled = true (per spec SEC-012)
- [ ] T126 [US2] Configure Key Vault enable_rbac_authorization = true (use RBAC vs access policies per plan)
- [ ] T127 [US2] Configure Key Vault secrets interface with vm_admin_password secret (name: var.vm_admin_secret_name, value: random_password.vm_admin_password.result)
- [ ] T128 [US2] Configure Key Vault diagnostic_settings sending logs to Log Analytics
- [ ] T129 [US2] Configure Key Vault lock via AVM module lock interface (CanNotDelete per spec SEC-011)
- [ ] T130 [US2] Add depends_on = [random_password.vm_admin_password]
- [ ] T131 [US2] [P] Implement terraform/outputs.tf key_vault_name output
- [ ] T132 [US2] [P] Implement terraform/outputs.tf key_vault_id output
- [ ] T133 [US2] [P] Implement terraform/outputs.tf key_vault_uri output
- [ ] T134 [US2] [P] Implement terraform/outputs.tf vm_admin_secret_name output (sensitive = true)

### Key Vault RBAC for Deployment Identity

- [ ] T135 [US2] Implement data.azurerm_client_config.current in terraform/main.tf
- [ ] T136 [US2] Implement azurerm_role_assignment for deployment identity in terraform/main.tf (role: Key Vault Secrets Officer, scope: Key Vault, principal_id: current identity)

### Update VM to Reference Key Vault Secret

- [ ] T137 [US2] Update VM module in terraform/main.tf to reference Key Vault secret for admin_password (module.key_vault.secrets[var.vm_admin_secret_name].value)
- [ ] T138 [US2] Update VM module depends_on to include module.key_vault and azurerm_role_assignment.kv_secrets_deployment

### Azure Bastion

- [ ] T139 [US2] Implement module block for Bastion in terraform/main.tf using Azure/avm-res-network-bastionhost/azurerm
- [ ] T140 [US2] Configure Bastion name = locals.bastion_name
- [ ] T141 [US2] Configure Bastion subnet_id referencing VNet module bastion_subnet (module.virtual_network.subnets["bastion_subnet"].id)
- [ ] T142 [US2] Configure Bastion sku = Basic (or Standard based on Phase 0 research cost analysis)
- [ ] T143 [US2] Configure Bastion lock via AVM module lock interface (CanNotDelete)
- [ ] T144 [US2] [P] Implement terraform/outputs.tf bastion_name output
- [ ] T145 [US2] [P] Implement terraform/outputs.tf bastion_id output

### Bastion Connection Instructions

- [ ] T146 [US2] Implement terraform/outputs.tf bastion_connect_instructions output with multi-line instructions (Portal navigation, username, password retrieval command)

### User Story 2 Deployment

- [ ] T147 [US2] Populate terraform/prod.tfvars with US2 variable values (vm_admin_secret_name)
- [ ] T148 [US2] Run terraform fmt -recursive
- [ ] T149 [US2] Run terraform validate
- [ ] T150 [US2] Run terraform plan -var-file=prod.tfvars -out=us2.tfplan (expect: Key Vault + secret + RBAC + Bastion + VM update)
- [ ] T151 [US2] Review plan output for US2 completeness (check Key Vault soft-delete, purge protection, secret stored, Bastion SKU)
- [ ] T152 [US2] Run terraform apply us2.tfplan
- [ ] T153 [US2] Verify Key Vault created with soft-delete and purge protection enabled
- [ ] T154 [US2] Verify Key Vault secret exists with name from vm_admin_secret_name variable
- [ ] T155 [US2] Retrieve password from Key Vault using Azure CLI: az keyvault secret show --name [secret-name] --vault-name [kv-name] --query value -o tsv
- [ ] T156 [US2] Verify Bastion deployed and connected to AzureBastionSubnet
- [ ] T157 [US2] Test RDP connection via Bastion (Portal β†’ VM β†’ Connect β†’ Bastion, use vmadmin username and Key Vault password)
- [ ] T158 [US2] Verify VM has no public IP address (confirm from VM properties)

**Phase 4 (US2) Validation**:
- βœ… Key Vault deployed with soft-delete and purge protection
- βœ… VM admin password stored in Key Vault secret
- βœ… Bastion deployed and connected to VNet
- βœ… RDP connection successful via Bastion using Key Vault password
- βœ… VM has no public IP address

**Parallel Execution Opportunities (US2)**:
- Tasks T118 (variable), T119-T120 (locals) can run in parallel
- Tasks T131-T134 (Key Vault outputs) can run in parallel
- Tasks T144-T145 (Bastion outputs) can run in parallel

---

## Phase 5: User Story 3 (P3) - Application Storage Integration

**Objective**: Provide secure access to Azure Files share for application data storage via private endpoint

**User Story**: Provide secure access to an Azure Files share for application data storage, connected via private endpoint to ensure data does not traverse the public internet.

**Independent Test**: From VM, mount Azure Files share using private endpoint IP. Verify data can be written to and read from the share without public internet connectivity.

**Prerequisites**: Phase 3 (US1) and Phase 4 (US2) complete - VM, VNet, and Key Vault deployed

### Variables for Storage

- [ ] T159 [P] [US3] Define variables.tf file_share_name variable (default: legacyappdata)
- [ ] T160 [P] [US3] Define variables.tf file_share_quota_gb variable (default: 1024, validation: must equal 1024 per spec clarifications)

### Locals for Storage Naming

- [ ] T161 [P] [US3] Define locals.tf storage_account_name (stavmlegacy{suffix}, lowercase alphanumeric, max 24 chars)
- [ ] T162 [P] [US3] Define locals.tf private_endpoint_name (pe-storage-avmlegacy-{suffix})

### Storage Account with File Share

- [ ] T163 [US3] Implement module block for Storage Account in terraform/main.tf using Azure/avm-res-storage-storageaccount/azurerm
- [ ] T164 [US3] Configure Storage Account name = locals.storage_account_name (lowercase alphanumeric)
- [ ] T165 [US3] Configure Storage Account account_kind = StorageV2
- [ ] T166 [US3] Configure Storage Account account_tier = Standard, account_replication_type = LRS (HDD per spec IC-007)
- [ ] T167 [US3] Configure Storage Account public_network_access_enabled = false (per spec SEC-009)
- [ ] T168 [US3] Configure Storage Account enable_infrastructure_encryption = true (per spec SEC-013)
- [ ] T169 [US3] Configure Storage Account file_shares with legacy_app_data share (name: var.file_share_name, quota: var.file_share_quota_gb, tier: TransactionOptimized)
- [ ] T170 [US3] Configure Storage Account private_endpoints interface for file subresource (check if built-in or need separate module per Phase 0 research Task 6)
- [ ] T171 [US3] Configure private endpoint name = locals.private_endpoint_name, subnet_resource_id = private_endpoint_subnet, subresource_names = ["file"]
- [ ] T172 [US3] Configure private endpoint private_dns_zone_group_name = "file-private-dns" (auto-create or existing zone per research)
- [ ] T173 [US3] Configure Storage Account diagnostic_settings sending logs to Log Analytics
- [ ] T174 [US3] Configure Storage Account lock via AVM module lock interface (CanNotDelete per spec SEC-011)
- [ ] T175 [US3] [P] Implement terraform/outputs.tf storage_account_name output
- [ ] T176 [US3] [P] Implement terraform/outputs.tf storage_account_id output
- [ ] T177 [US3] [P] Implement terraform/outputs.tf file_share_name output

### File Share Mount Instructions

- [ ] T178 [US3] Implement terraform/outputs.tf file_share_mount_instructions output with PowerShell commands for mounting from VM

### User Story 3 Deployment

- [ ] T179 [US3] Populate terraform/prod.tfvars with US3 variable values (file_share_name, file_share_quota_gb)
- [ ] T180 [US3] Run terraform fmt -recursive
- [ ] T181 [US3] Run terraform validate
- [ ] T182 [US3] Run terraform plan -var-file=prod.tfvars -out=us3.tfplan (expect: Storage Account + file share + private endpoint)
- [ ] T183 [US3] Review plan output for US3 completeness (check public_network_access = false, file share quota = 1TB, private endpoint connected)
- [ ] T184 [US3] Run terraform apply us3.tfplan
- [ ] T185 [US3] Verify Storage Account created with Standard_LRS replication
- [ ] T186 [US3] Verify file share created with 1TB quota (1024GB)
- [ ] T187 [US3] Verify Storage Account has public network access disabled
- [ ] T188 [US3] Verify private endpoint created and connected to private_endpoint_subnet
- [ ] T189 [US3] Test file share mounting from VM via Bastion RDP session (use output file_share_mount_instructions)
- [ ] T190 [US3] From VM: Run net use Z: \\[storage-account-name].privatelink.file.core.windows.net\[file-share-name]
- [ ] T191 [US3] From VM: Test write access by creating a test file on Z: drive
- [ ] T192 [US3] From VM: Test read access by reading the test file from Z: drive
- [ ] T193 [US3] Verify DNS resolution from VM resolves storage FQDN to private endpoint IP (not public IP)

**Phase 5 (US3) Validation**:
- βœ… Storage Account deployed with Standard_LRS, no public access
- βœ… File share created with 1TB quota
- βœ… Private endpoint deployed and connected to VNet
- βœ… File share accessible from VM via private endpoint (mount successful)
- βœ… Read/write operations successful on mounted file share
- βœ… DNS resolves to private IP (not public IP)

**Parallel Execution Opportunities (US3)**:
- Tasks T159-T160 (variables) can run in parallel
- Tasks T161-T162 (locals) can run in parallel
- Tasks T175-T177 (outputs) can run in parallel

---

## Phase 6: User Story 4 (P4) - Internet Access and Observability

**Objective**: Enable outbound internet access via NAT Gateway and implement comprehensive monitoring through Log Analytics with critical alerts

**User Story**: Enable outbound internet access via NAT Gateway for Windows Updates and patches, and implement comprehensive monitoring through Log Analytics with diagnostic logging and critical alerts.

**Independent Test**: From VM, verify outbound internet connectivity (e.g., download Windows Update). Confirm diagnostic logs flowing to Log Analytics and test alerts trigger correctly.

**Prerequisites**: Phase 3 (US1), Phase 4 (US2), Phase 5 (US3) complete - All infrastructure deployed

### Variables for NAT Gateway and Alerts

- [ ] T194 [P] [US4] Define variables.tf alert_action_group_email variable (no default - must be provided in tfvars)

### Locals for NAT Gateway and Alerts

- [ ] T195 [P] [US4] Define locals.tf nat_gateway_name (nat-avmlegacy-{suffix})
- [ ] T196 [P] [US4] Define locals.tf nat_public_ip_name (pip-nat-avmlegacy-{suffix})
- [ ] T197 [P] [US4] Define locals.tf action_group_name (ag-avmlegacy-{suffix})

### NAT Gateway

- [ ] T198 [US4] Implement module block for NAT Gateway in terraform/main.tf using Azure/avm-res-network-natgateway/azurerm
- [ ] T199 [US4] Configure NAT Gateway name = locals.nat_gateway_name
- [ ] T200 [US4] Configure NAT Gateway public_ip_addresses with pip-nat public IP (name: locals.nat_public_ip_name, zones: [var.availability_zone])
- [ ] T201 [US4] Configure NAT Gateway subnet_associations with vm_subnet (subnet_id from module.virtual_network.subnets["vm_subnet"].id)
- [ ] T202 [US4] [P] Implement terraform/outputs.tf nat_gateway_name output
- [ ] T203 [US4] [P] Implement terraform/outputs.tf nat_gateway_public_ip output

### Action Group for Alerts

- [ ] T204 [US4] Implement azurerm_monitor_action_group in terraform/main.tf (name: locals.action_group_name, short_name: avmalerts)
- [ ] T205 [US4] Configure action group email_receiver (name: admin-email, email_address: var.alert_action_group_email)

### Metric Alerts

- [ ] T206 [P] [US4] Implement azurerm_monitor_metric_alert for VM stopped in terraform/main.tf (name: alert-vm-stopped-{vm_name}, scope: VM ID)
- [ ] T207 [US4] Configure VM stopped alert criteria (metric: VmAvailabilityMetric, aggregation: Average, operator: LessThan, threshold: 1, severity: 0)
- [ ] T208 [US4] Configure VM stopped alert frequency = PT5M, window_size = PT5M
- [ ] T209 [US4] Configure VM stopped alert action referencing action group
- [ ] T210 [P] [US4] Implement azurerm_monitor_metric_alert for VM disk usage in terraform/main.tf (name: alert-vm-disk-usage-{vm_name}, scope: VM ID)
- [ ] T211 [US4] Configure VM disk alert criteria (metric: OS Disk Used Percent, aggregation: Average, operator: GreaterThan, threshold: 90, severity: 0)
- [ ] T212 [US4] Configure VM disk alert frequency = PT15M, window_size = PT15M
- [ ] T213 [US4] Configure VM disk alert action referencing action group
- [ ] T214 [P] [US4] Implement azurerm_monitor_metric_alert for Key Vault access failures in terraform/main.tf (name: alert-kv-access-failures-{kv_name}, scope: Key Vault ID)
- [ ] T215 [US4] Configure Key Vault alert criteria (metric: ServiceApiResult, aggregation: Count, operator: GreaterThan, threshold: 0, severity: 0)
- [ ] T216 [US4] Configure Key Vault alert dimension filter (name: StatusCode, operator: Include, values: ["403"])
- [ ] T217 [US4] Configure Key Vault alert frequency = PT5M, window_size = PT5M
- [ ] T218 [US4] Configure Key Vault alert action referencing action group

### User Story 4 Deployment

- [ ] T219 [US4] Populate terraform/prod.tfvars with US4 variable values (alert_action_group_email - **UPDATE THIS**)
- [ ] T220 [US4] Run terraform fmt -recursive
- [ ] T221 [US4] Run terraform validate
- [ ] T222 [US4] Run terraform plan -var-file=prod.tfvars -out=us4.tfplan (expect: NAT Gateway + public IP + action group + 3 alerts)
- [ ] T223 [US4] Review plan output for US4 completeness (check NAT Gateway associated with vm_subnet, alerts configured with correct thresholds)
- [ ] T224 [US4] Run terraform apply us4.tfplan
- [ ] T225 [US4] Verify NAT Gateway deployed with public IP
- [ ] T226 [US4] Verify NAT Gateway associated with vm_subnet
- [ ] T227 [US4] Test outbound internet connectivity from VM via Bastion RDP session (Invoke-WebRequest -Uri "https://www.microsoft.com" -UseBasicParsing)
- [ ] T228 [US4] Verify VM cannot receive inbound connections from internet (remains inaccessible)
- [ ] T229 [US4] Verify diagnostic logs from VM in Log Analytics (run query: Perf | where Computer startswith "vm-avmlegacy" | take 10)
- [ ] T230 [US4] Verify diagnostic logs from Key Vault in Log Analytics (run query: AzureDiagnostics | where ResourceType == "VAULTS" | take 10)
- [ ] T231 [US4] Verify diagnostic logs from Storage Account in Log Analytics
- [ ] T232 [US4] Test VM stopped alert by stopping VM in Portal (wait 5 minutes, verify alert notification)
- [ ] T233 [US4] Test alert action group email notification received
- [ ] T234 [US4] Start VM after alert test

**Phase 6 (US4) Validation**:
- βœ… NAT Gateway deployed and associated with vm_subnet
- βœ… VM has outbound internet access via NAT Gateway
- βœ… VM remains inaccessible from internet (inbound blocked)
- βœ… Diagnostic logs flowing to Log Analytics from VM, Key Vault, Storage Account
- βœ… 3 metric alerts configured and functional (VM stopped, disk >90%, Key Vault failures)
- βœ… Alert notifications delivered to action group

**Parallel Execution Opportunities (US4)**:
- Tasks T195-T197 (locals) can run in parallel
- Tasks T202-T203 (NAT Gateway outputs) can run in parallel
- Tasks T206, T210, T214 (alert resource creation) can run in parallel after action group created

---

## Phase 7: Polish & Cross-Cutting Concerns

**Objective**: Final validation, documentation, cost analysis, and CI/CD pipeline setup (optional)

**Prerequisites**: All user stories (US1-US4) complete and validated

### Final Terraform Validation

- [ ] T235 Run terraform fmt -recursive -check to ensure all files formatted
- [ ] T236 Run terraform validate to ensure no syntax errors
- [ ] T237 Run tfsec . to scan for security issues (expect: zero HIGH or CRITICAL findings per spec SC-009)
- [ ] T238 Run checkov -d . to scan for compliance issues
- [ ] T239 Address any HIGH or CRITICAL findings from security scans

### Cost Validation

- [ ] T240 Use Azure Pricing Calculator to estimate monthly cost of deployed infrastructure
- [ ] T241 Verify estimated cost is under $200/month per spec SC-013
- [ ] T242 Document cost breakdown in docs/README.md (VM, Bastion, NAT Gateway, Log Analytics, Storage, Key Vault)

### Documentation Finalization

- [ ] T243 Update terraform/README.md with complete deployment instructions (prerequisites, backend setup, variable customization, deployment steps)
- [ ] T244 Update docs/README.md with project overview (architecture summary, cost estimate, deployment time estimate)
- [ ] T245 Update docs/architecture.md with infrastructure diagram (VNet topology, resource relationships, security boundaries)
- [ ] T246 Document all outputs in terraform/README.md (how to retrieve VM password, connect via Bastion, mount file share)
- [ ] T247 Create terraform/prod.tfvars.example with all variables and rich comments (remove sensitive values)

### Deployment Success Criteria Verification

- [ ] T248 Verify SC-001: Infrastructure deployment completed within 30 minutes (time terraform apply)
- [ ] T249 Verify SC-002: RDP connection via Bastion established within 2 minutes
- [ ] T250 Verify SC-003: VM password from Key Vault successfully authenticates RDP session (100% success rate)
- [ ] T251 Verify SC-004: VM can mount Azure Files share and perform read/write operations
- [ ] T252 Verify SC-005: VM can download content from internet via NAT Gateway (test Windows Update or HTTP GET)
- [ ] T253 Verify SC-006: VM is NOT reachable via direct internet connection (external port scan shows no open ports)
- [ ] T254 Verify SC-007: Diagnostic logs from VM, Key Vault, Storage appear in Log Analytics within 15 minutes
- [ ] T255 Verify SC-008: Critical alerts trigger within 5 minutes (test VM stopped alert)
- [ ] T256 Verify SC-009: terraform fmt -check, terraform validate, tfsec pass with zero HIGH/CRITICAL findings
- [ ] T257 Verify SC-010: All values from terraform.tfvars (no hardcoded values in main.tf)
- [ ] T258 Verify SC-011: AVM module variables correctly structured per module documentation
- [ ] T259 Verify SC-012: VM computer name is 15 characters or fewer
- [ ] T260 Verify SC-013: Total monthly cost under $200/month

### Optional CI/CD Pipeline

- [ ] T261 [P] Create .github/workflows/terraform-validate.yml for CI/CD pipeline
- [ ] T262 [P] Configure pipeline to run terraform fmt -check on pull requests
- [ ] T263 [P] Configure pipeline to run terraform validate on pull requests
- [ ] T264 [P] Configure pipeline to run tfsec scan on pull requests
- [ ] T265 [P] Configure pipeline to fail on HIGH or CRITICAL security findings
- [ ] T266 [P] Test pipeline by creating a test pull request

### Final Acceptance

- [ ] T267 Execute complete teardown and redeploy to verify infrastructure is fully recreatable (terraform destroy β†’ terraform apply)
- [ ] T268 Time full deployment from scratch (should complete within 30 minutes per SC-001)
- [ ] T269 Document actual deployment time and cost in docs/README.md

**Phase 7 Validation**:
- βœ… All 13 success criteria (SC-001 through SC-013) met
- βœ… Terraform validation passing (fmt, validate, tfsec, checkov)
- βœ… Cost under $200/month confirmed
- βœ… Complete documentation available (README, architecture, deployment guide)
- βœ… Infrastructure fully recreatable from Terraform

---

## Dependencies Summary

### Phase Dependencies
```
Phase 1 (Setup)
  ↓
Phase 2 (Foundational) [BLOCKS all user stories]
  ↓
Phase 3 (US1 - Core Compute) [BLOCKS all subsequent stories]
  ↓
Phase 4 (US2 - Secure Access) [Independent of US3, US4]
  ↓
Phase 5 (US3 - Storage) [Independent of US4, depends on US1+US2]
  ↓
Phase 6 (US4 - Observability) [Depends on US1+US2+US3]
  ↓
Phase 7 (Polish) [Depends on all user stories]
```

### Critical Path
1. Setup (Phase 1) β†’ Foundational (Phase 2) β†’ US1 (Phase 3) β†’ US2 (Phase 4) β†’ US3 (Phase 5) β†’ US4 (Phase 6) β†’ Polish (Phase 7)

### Parallel Opportunities
- **Setup Phase**: Tasks T005-T010 (Terraform file shells), T014-T016 (tooling installation)
- **Foundational Phase**: Variable definitions (T027-T030), outputs (T038-T039)
- **US1 Phase**: Variable definitions (T047-T054), NSG module blocks (T070, T074, T080), NSG associations (T084-T086), outputs (T068-T069, T103-T106)
- **US2 Phase**: Variable/locals (T118-T120), Key Vault outputs (T131-T134), Bastion outputs (T144-T145)
- **US3 Phase**: Variables (T159-T160), locals (T161-T162), outputs (T175-T177)
- **US4 Phase**: Locals (T195-T197), NAT Gateway outputs (T202-T203), alert resource creation (T206, T210, T214)
- **Polish Phase**: CI/CD pipeline tasks (T261-T266)

---

## Notes

### Tests
- **Not Generated**: Per spec, no test infrastructure requested. User story acceptance scenarios serve as manual test criteria.

### MVP Recommendation
- **Suggested MVP**: Complete through Phase 4 (US1 + US2) for minimum viable secure infrastructure (VM with RDP access via Bastion, password in Key Vault)
- **Optional for MVP**: US3 (Storage) and US4 (Observability) can be deferred

### File Share Mount
- **Post-Deployment Manual Step**: File share mounting to VM is manual per spec clarifications (no automation, aligns with IaC-first principle)
- **Instructions**: Provided in terraform/outputs.tf file_share_mount_instructions output

### AVM Module Versions
- **Research Required**: Phase 0 research (plan.md Phase 0) must verify latest AVM module versions from Terraform Registry before implementation
- **Version Constraints**: Use pessimistic versioning (~> X.Y.0) per constitution Principle II

### Constitution Compliance
- **All tasks align with**: Constitution Principles I-V (Terraform-first, AVM-only, Security/Reliability, Single-template, Validation-first)
- **No local modules**: All tasks use AVM modules exclusively (no custom module creation)

### Validation Gates
- **Between Phases**: Run terraform fmt β†’ validate β†’ plan β†’ review before apply
- **Security Scanning**: Run tfsec and checkov before final deployment
- **Manual Testing**: Each user story has independent test criteria in spec.md

---

**End of Tasks Document**

**Total Tasks**: 269
**Estimated Implementation Time**: 20-30 hours (including research, implementation, testing, documentation)
**Deployment Time**: ~30 minutes per spec SC-001
**Cost**: <$200/month per spec SC-013
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your task list, with a comment of something like Tasks generated.

7. Analyze (Optional)

Spec Kit uses /speckit.analyze to generate an analysis report. The prompt doesn’t require any specific inputs as it analyzes the existing spec, plan and tasks to produce the report.

Info

To learn more about the analyze step, see the Analyze chapter in the Spec Kit article.

  1. Run the following prompt to generate an analysis report for our example:
/speckit.analyze
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see something like this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Specification Analysis Report

**Feature**: 001-legacy-vm-workload
**Analyzed Files**: spec.md, plan.md, tasks.md, constitution.md
**Date**: 2026-01-26
**Analysis Mode**: READ-ONLY (no modifications applied)

---

## Executive Summary

Analyzed **24 functional requirements**, **4 user stories**, and **61 implementation tasks** across the specification artifacts. Identified **16 findings** (0 CRITICAL, 5 HIGH, 8 MEDIUM, 3 LOW). Constitution compliance: **100% aligned** with all 5 principles satisfied. Primary concerns: **ambiguous terminology** (Standard HDD), **missing technical specifications** (AVM module versions, NSG rules, encryption details), and **underspecified monitoring thresholds**.

**Overall Assessment**: βœ… **SAFE TO PROCEED** with implementation after resolving 5 HIGH severity issues. No CRITICAL or constitution-blocking issues detected.

---

## Findings

| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Ambiguity | HIGH | spec.md:FR-003, FR-008, tasks.md:T025, T030, T033 | "Standard HDD" not mapped to explicit Azure SKU (Standard_LRS vs StandardSSD_LRS) | Specify `Standard_LRS` explicitly in spec FR-003, FR-008; update tasks T025, T030, T033 with SKU name |
| A2 | Underspecification | HIGH | spec.md:FR-022, tasks.md:T040 | "Disk utilization >90%" threshold ambiguous - percentage of what? (capacity, IOPS, throughput) | Clarify as "OS disk used capacity >90%" or "Data disk used capacity >90%"; specify which disk(s) |
| A3 | Underspecification | HIGH | spec.md:FR-020, plan.md, tasks.md:T012 | NSG rules incomplete - missing explicit allow rules for Key Vault (443), storage (445), Log Analytics (443) | Document required outbound NSG rules: Azure services (AzureCloud service tag) port 443 for KV/Storage/LA |
| A4 | Coverage Gap | HIGH | spec.md:FR-011, tasks.md:T013-T015 | Password complexity requirements not validated against Azure VM password policy (12-123 chars, 3 of 4 types) | Add validation task or clarify password generation formula meets Azure requirements |
| A5 | Underspecification | HIGH | All documents | AVM module version constraints missing - no "latest stable" definition or version pinning strategy | Add research task to query MCR for latest versions; specify version pinning strategy (exact vs ^0.x) |
| U1 | Underspecification | MEDIUM | spec.md:FR-019, tasks.md:T016, T020, T027, T036 | Diagnostic log categories not specified - which logs to collect per resource type? | Define log categories: VM (Performance, Security), KV (AuditEvent), Storage (Transaction, StorageRead) |
| U2 | Inconsistency | MEDIUM | spec.md:US4 acceptance scenario, plan.md | Key Vault access model conflict - spec mentions "access policies", plan says "RBAC". Which to use? | Resolve: Use RBAC per plan (T015 specifies enableRbacAuthorization: true); update spec to remove access policies reference |
| U3 | Coverage Gap | MEDIUM | spec.md, tasks.md | Encryption-at-rest requirements implicit but not explicit for VM disks, storage account, Key Vault | Add requirement: "All storage must use Azure-managed encryption at rest" or assume default encryption |
| U4 | Underspecification | MEDIUM | spec.md:FR-022, tasks.md:T039-T041 | Alert notification destinations undefined - who receives alerts? Email? Action group? | Document assumption: Alert rules created without notification actions (configured post-deployment) |
| U5 | Inconsistency | MEDIUM | spec.md:FR-016, plan.md, tasks.md:T007 | Availability zone terminology: spec says "selection", plan/tasks say "parameter" - is it user choice or fixed? | Clarify: availabilityZone is a parameter with default 1; user can select 1, 2, or 3 (not random assignment) |
| U6 | Underspecification | MEDIUM | spec.md:FR-024, plan.md | File share growth monitoring "documented procedures" - what procedures? Manual check? Alert? | Specify: Document manual procedure to query file share usage via Azure CLI/Portal; no automated monitoring |
| U7 | Coverage Gap | MEDIUM | spec.md, tasks.md:T028 | VM boot diagnostics storage account not specified - uses managed storage or custom? | Add clarification: Use managed boot diagnostics (no separate storage account); T028 should specify this |
| U8 | Underspecification | MEDIUM | spec.md:FR-014, plan.md, tasks.md:T009 | Random suffix generation uses uniqueString(resourceGroup().id) - what's the character length? | Specify: 6-character suffix using substring(uniqueString(resourceGroup().id), 0, 6) |
| T1 | Terminology Drift | LOW | spec.md uses "Azure Storage Account", tasks.md uses "Storage Account" | Minor inconsistency in terminology - not functionally impactful | Standardize on "Storage Account" throughout |
| T2 | Duplication | LOW | spec.md:FR-005, FR-017 + plan.md Technical Context | Resource group designation repeated - spec says "production", plan implies via parameter 'environment' | Consolidate: Resource group is deployment target (not created by template); environment tag controlled by parameter |
| T3 | Coverage Gap | LOW | tasks.md:T002 | No task detail for creating bicepconfig.json analyzer rules despite constitution principle IV requiring it | Add detail to T002: Include analyzer rule `use-recent-module-versions: warning` in bicepconfig.json |

---

## Coverage Summary

### Requirements-to-Tasks Mapping

| Requirement Key | Has Task? | Task IDs | Coverage Status |
|-----------------|-----------|----------|-----------------|
| FR-001 (WinServer2016 US West 3) | βœ… | T006, T023 | βœ… Covered by location param + imageReference |
| FR-002 (2 cores, 8GB RAM) | βœ… | T023 | βœ… vmSize parameter (Standard_D2s_v3) |
| FR-003 (Standard HDD OS) | βœ… | T025 | ⚠️ Ambiguous - need SKU clarification (A1) |
| FR-004 (500GB data disk) | βœ… | T030 | ⚠️ Ambiguous - need SKU clarification (A1) |
| FR-005 (Single RG) | βœ… | Implicit | βœ… Deployment target (no task needed) |
| FR-006 (VNet) | βœ… | T011 | βœ… 3 subnets defined |
| FR-007 (Bastion) | βœ… | T018-T021 | βœ… Fully covered |
| FR-008 (Storage + file share) | βœ… | T032-T033 | ⚠️ Ambiguous SKU (A1) |
| FR-009 (Private endpoint) | βœ… | T035 | βœ… Covered |
| FR-010 (Key Vault) | βœ… | T014 | βœ… Covered |
| FR-011 (Password gen + store) | βœ… | T013, T015 | ⚠️ Complexity validation gap (A4) |
| FR-012 (Username = administrator) | βœ… | T024, T053 | βœ… Covered |
| FR-013 (Secret name param) | βœ… | T015, T053 | βœ… Covered |
| FR-014 (Naming convention) | βœ… | T009 + all resources | ⚠️ Length not specified (U8) |
| FR-015 (AVM modules only) | βœ… | All module tasks | ⚠️ No version constraints (A5) |
| FR-016 (Zones 1-3) | βœ… | T007 | ⚠️ Terminology inconsistency (U5) |
| FR-017 (Parameter-driven) | βœ… | T005-T008, T053 | βœ… Covered |
| FR-018 (CanNotDelete locks) | βœ… | T017, T021, T029, T031, T037, T038 | βœ… Covered |
| FR-019 (Diagnostic logging) | βœ… | T016, T020, T027, T036 | ⚠️ Log categories unspecified (U1) |
| FR-020 (NSG rules) | βœ… | T012 | ⚠️ Incomplete rules (A3) |
| FR-021 (No public access) | βœ… | T034 | βœ… Covered |
| FR-022 (Critical alerts) | βœ… | T039-T041 | ⚠️ Threshold + notification gaps (A2, U4) |
| FR-023 (Log Analytics) | βœ… | T010 | βœ… Covered |
| FR-024 (File share growth) | βœ… | Documented in spec | ⚠️ Procedure undefined (U6) |

**Coverage %**: **100%** (24/24 requirements have associated tasks)

### User Story Coverage

| User Story | Priority | Tasks | Coverage Status |
|------------|----------|-------|-----------------|
| US1 - Core VM Infrastructure | P1 (MVP) | T018-T021, T022-T029 | βœ… Fully covered (9 tasks) |
| US2 - Attach Additional Storage | P2 | T030-T031 | βœ… Fully covered (2 tasks) |
| US3 - Connect to Secure File Share | P3 | T032-T038 | βœ… Fully covered (7 tasks) |
| US4 - Secure Secret Management | P1 (MVP) | T013-T017 | βœ… Fully covered (5 tasks) |

**User Story Coverage**: 100% (all 4 stories have complete task coverage)

---

## Constitution Alignment Issues

**Status**: βœ… **ZERO VIOLATIONS** - All 5 constitution principles are fully satisfied

| Principle | Status | Evidence | Risk Level |
|-----------|--------|----------|------------|
| I. AVM-Only Modules | βœ… PASS | Tasks T010-T041 reference only AVM modules from br/public:avm/... registry; zero direct resource declarations planned | None |
| II. IaC-First Approach | βœ… PASS | Single main.bicep (T004), no custom scripts/CSE, ARM incremental mode, manual post-deployment file share mounting (T058) | None |
| III. Security & Reliability | βœ… PASS | Private endpoints (T035), Key Vault (T014-T015), CanNotDelete locks (T017, T021, T029, T031, T037, T038), diagnostic logs (T016, T020, T027, T036), NSG (T012) | None |
| IV. Pre-Deployment Validation | βœ… PASS | Tasks T054-T055 explicitly mandate `az deployment group validate` + ARM What-If review before deployment (T056) | None |
| V. Naming Convention & Regional Standards | βœ… PASS | Naming via T009 (uniqueString suffix), location=westus3 (T006), zones 1-3 (T007), CAF abbreviations in all resource names | None |

**Constitution Compliance Score**: 100% (5/5 principles satisfied)

---

## Unmapped Tasks

**Status**: βœ… **ZERO UNMAPPED TASKS**

All 61 tasks map to at least one requirement, user story, or infrastructure prerequisite:
- **Setup tasks (T001-T003)**: Infrastructure initialization (required for all user stories)
- **Foundational tasks (T004-T012)**: Shared resources (Log Analytics, VNet, NSG) - prerequisite for all user stories
- **User story tasks (T013-T038)**: Direct mapping to US1, US2, US3, US4
- **Polish tasks (T039-T053)**: Cross-cutting concerns (alerts, outputs, parameters)
- **Validation tasks (T054-T061)**: Post-implementation verification

---

## Metrics

### Quantitative Analysis

- **Total Functional Requirements**: 24 (FR-001 through FR-024)
- **Total User Stories**: 4 (US1-P1, US2-P2, US3-P3, US4-P1)
- **Total Implementation Tasks**: 61 (T001 through T061)
- **Total Entities (Data Model)**: 12 Azure resources
- **Total Input Parameters**: 11 (3 required, 8 optional with defaults)
- **Total Output Values**: 10

### Coverage Metrics

- **Requirements Coverage**: 100% (24/24 requirements have β‰₯1 task)
- **User Story Coverage**: 100% (4/4 stories have complete task sets)
- **Constitution Compliance**: 100% (5/5 principles satisfied)
- **Task-to-Requirement Traceability**: 100% (all 61 tasks map to requirements or prerequisites)

### Quality Metrics

- **Ambiguity Count**: 5 findings (A1, A2, U1, U2, U8)
- **Duplication Count**: 1 finding (T2)
- **Underspecification Count**: 9 findings (A2, A3, A4, A5, U1, U4, U6, U7, U8)
- **Coverage Gaps**: 4 findings (A4, U3, U7, T3)
- **Inconsistencies**: 2 findings (U2, U5)
- **Terminology Drift**: 1 finding (T1)

### Severity Distribution

- **CRITICAL Issues**: 0 (deployment blockers)
- **HIGH Issues**: 5 (implementation ambiguity, missing technical specs)
- **MEDIUM Issues**: 8 (specification gaps, inconsistencies)
- **LOW Issues**: 3 (documentation polish, minor gaps)
- **Total Findings**: 16

---

## Dependency Analysis

### Critical Path Dependencies

```
Log Analytics (T010)
    ↓
VNet + NSG (T011-T012) ────┐
    ↓                       β”‚
Key Vault + Password ────────
(T013-T017)                 β”‚
    ↓                       β”‚
VM + Bastion β†β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
(T018-T029)
    ↓
Data Disk (T030-T031)
    ↓
Storage + Private Endpoint (T032-T038)
    ↓
Alerts + Outputs (T039-T051)
    ↓
Validation (T054-T061)
```

### Parallelization Opportunities

**35 tasks marked [P]** can execute in parallel:

1. **Phase 2 - Foundational** (after Log Analytics + VNet):
  - T005-T008 (Parameters) - 4 tasks in parallel
  - T012 (NSG) can overlap with T010-T011

2. **Phase 3 - MVP** (after Key Vault complete):
  - T018-T021 (Bastion) parallel with T022-T029 (VM) - both depend on VNet only

3. **Phase 4-5** (after Foundation):
  - T030-T031 (Data Disk) parallel with T032-T038 (Storage + PE) - no mutual dependency

4. **Phase 6 - Polish**:
  - T039-T041 (Alert Rules) - 3 tasks in parallel
  - T042-T051 (Outputs) - 10 tasks in parallel
  - T052-T053 (Parameters file) parallel with T039-T051

**Estimated Parallelization Benefit**: ~30% time reduction (from 8.5 hours to ~6 hours with optimal parallelization)

---

## Next Actions

### Before `/speckit.implement` (MANDATORY)

**1. RESOLVE HIGH SEVERITY ISSUES (A1-A5)**:

- **A1 - Standard HDD SKU Ambiguity**:
  - Update spec.md FR-003: "Virtual machine MUST use Standard HDD storage tier (Standard_LRS SKU) for the OS disk"
  - Update spec.md FR-008: "System MUST deploy an Azure Storage Account with HDD-backed file share (Standard_LRS SKU) with 1TB initial quota"
  - Update tasks.md T025: "Configure VM OS disk with Standard_LRS storage SKU (Standard HDD)"
  - Update tasks.md T030: "Configure VM data disks array with 500GB disk, Standard_LRS (Standard HDD)"
  - Update tasks.md T033: "Configure Storage Account with Standard_LRS SKU"

- **A2 - Disk Utilization Alert Threshold**:
  - Update spec.md FR-022: "System MUST configure critical alerts for VM stopped, OS disk used capacity >90% OR data disk used capacity >90%, and Key Vault access failures"
  - Update tasks.md T040: "Add Metric Alert Rule for OS disk used capacity >90% AND data disk used capacity >90% with separate alert conditions"

- **A3 - NSG Rules Incomplete**:
  - Update spec.md FR-020 to specify: "NSG MUST allow outbound traffic to AzureCloud service tag on port 443 (for Key Vault, Storage, Log Analytics); MUST allow inbound RDP (3389) from AzureBastionSubnet only; MUST deny all other inbound traffic from Internet"
  - Update tasks.md T012: "Add Network Security Group module with rules: Priority 100 Allow RDP inbound from AzureBastionSubnet, Priority 110 Allow HTTPS outbound to AzureCloud tag, Priority 4096 Deny all inbound from Internet"

- **A4 - Password Complexity Validation**:
  - Add assumption to spec.md Assumptions section: "Generated password formula '${uniqueString(resourceGroup().id)}${guid(subscription().id, resourceGroup().id)}A1!' produces compliant passwords meeting Azure VM requirements (12-123 characters, contains uppercase, lowercase, number, special character)"
  - OR add validation task between T013-T014: Verify password generation meets Azure complexity rules

- **A5 - AVM Module Versioning**:
  - Add research task to research.md: Query MCR tags for latest stable versions of all 10 AVM modules
  - Add versioning strategy to plan.md Technical Context: "Use exact version pinning (e.g., 0.10.2) for production deployments to ensure reproducibility"
  - Update all tasks T010-T041 to specify version format: `br/public:avm/res/<module>:0.x.x` (replace with actual versions)

### Recommended (MEDIUM Severity Issues - U1-U8)

These can be addressed during implementation but are recommended before finalizing:

- **U1**: Specify diagnostic log categories per resource type in research.md or assume defaults
- **U2**: Update spec.md US4 acceptance scenario 3 to replace "access policies" with "RBAC" for consistency
- **U3**: Add encryption-at-rest requirement or assumption (Azure default encryption assumed)
- **U4**: Add assumption to spec.md: "Alert notification actions (email, webhook, action groups) configured post-deployment"
- **U5**: Clarify in spec.md FR-016: "Availability zone MUST be user-selectable via parameter (values 1, 2, or 3)"
- **U6**: Document in quickstart.md: "Monitor file share usage via Azure Portal β†’ Storage Account β†’ File Shares β†’ Properties β†’ Quota"
- **U7**: Update tasks.md T028: "Enable VM boot diagnostics using managed storage (no separate storage account)"
- **U8**: Update tasks.md T009: "Create random suffix variable using substring(uniqueString(resourceGroup().id), 0, 6)"

### Optional (LOW Severity Issues - T1-T3)

Address during implementation or final polish:

- **T1**: Standardize terminology to "Storage Account" throughout all documents
- **T2**: Clarify in plan.md that environment parameter is for tagging only (resource group is deployment target)
- **T3**: Expand tasks.md T002 description: "Create infra/bicepconfig.json with AVM registry alias and analyzer rule use-recent-module-versions: warning"

---

## Deployment Readiness Assessment

### Overall Readiness: ⚠️ **CONDITIONALLY READY**

**Recommendation**: βœ… **SAFE TO PROCEED** with implementation **AFTER** resolving 5 HIGH severity issues (A1-A5)

**Justification**:
- βœ… **Strengths**:
  - Zero CRITICAL blockers
  - 100% constitution compliance
  - Complete requirements-to-task coverage
  - Well-structured user story organization with clear MVP definition
  - Comprehensive dependency analysis with parallelization opportunities
  - No unmapped tasks or orphaned requirements

- ⚠️ **Risks**:
  - HIGH issues create implementation ambiguity (developer may choose wrong SKU, incomplete NSG rules)
  - Missing AVM version specifications could lead to breaking changes or deployment failures
  - Underspecified thresholds (disk utilization) may trigger false alerts or miss real issues

- βœ… **Mitigation**:
  - All HIGH issues are resolvable through specification updates (no architecture changes required)
  - Estimated remediation time: 30-45 minutes
  - No blocking dependencies or conflicting requirements detected

### Pre-Implementation Checklist

Before running `/speckit.implement`, ensure:

- [ ] All 5 HIGH severity issues (A1-A5) resolved in spec.md and tasks.md
- [ ] Constitution compliance maintained (currently 100%)
- [ ] AVM module versions researched and documented
- [ ] NSG rules fully specified with priorities and service tags
- [ ] Azure VM password complexity requirements validated or assumed

### Implementation Risk Level: **LOW-MEDIUM**

With HIGH issues resolved, implementation risk drops to LOW. Current specification provides sufficient clarity for experienced Bicep developer but requires technical gap-filling that could introduce inconsistencies if not addressed upfront.

---

## Remediation Options

**Would you like concrete remediation edits for the 5 HIGH severity issues (A1-A5)?**

Options:
- **A. Yes, provide all edits** - Generate specific text replacements for spec.md, plan.md, tasks.md to resolve all HIGH issues
- **B. Only critical clarifications** - Provide edits for A1-A3 (SKU, alerts, NSG) only; defer A4-A5 for implementation phase
- **C. Manual remediation** - Keep analysis report as-is; I'll manually address issues based on recommendations

---

## Appendix: Document Statistics

### Specification Artifacts

| Document | Lines | Sections | Key Content |
|----------|-------|----------|-------------|
| constitution.md | 114 | 5 principles | Non-negotiable rules; 100% compliance |
| spec.md | 169 | 4 user stories, 24 FRs, 13 success criteria | Requirements definition; 2 clarifications |
| plan.md | 200 | Technical context, constitution checks, Phase 0-1 summary | Implementation blueprint; 6 design artifacts |
| data-model.md | 394 | 12 entities, ERD, deployment order | Resource definitions; dependency mapping |
| research.md | 236 | 8 research topics | AVM mapping, password strategy, network design |
| contracts/parameters.md | 317 | 11 parameters | Input contract; 3 required, 8 optional |
| contracts/outputs.md | 234 | 10 outputs | Output contract; administrative + operational |
| quickstart.md | ~300 | 7-step deployment guide | Deployment procedures; troubleshooting |
| tasks.md | 277 | 61 tasks across 7 phases | Implementation checklist; MVP + incremental delivery |

### Total Documentation Volume

- **Total Lines**: ~2,241 lines
- **Total Artifacts**: 9 files (+ checklists/implementation-readiness.md)
- **Estimated Reading Time**: 45-60 minutes
- **Estimated Implementation Time**: 8.5 hours (6 hours with parallelization)

---

**Report Generated**: 2026-01-26
**Next Action**: Resolve HIGH severity issues β†’ Run `/speckit.implement` β†’ Deploy to test environment
/speckit.analyze
βž• Expand to see the results

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

In the Copilot chat window, you should see something like this:
Specify Bootstrap

<!-- markdownlint-disable -->
# Specification Analysis Report

**Feature**: 001-my-legacy-workload
**Analysis Date**: 2026-02-18
**Analyzed Artifacts**: spec.md, plan.md, tasks.md, constitution.md
**Status**: βœ… **CRITICAL/HIGH ISSUES RESOLVED** - Ready for Implementation

---

## Executive Summary

Analyzed 3 core artifacts (spec.md, plan.md, tasks.md) and constitution.md before implementation. Found **31 findings** across 6 detection categories.

**Critical Issues**: 2 (resolved)
**High Issues**: 4 (resolved)
**Medium Issues**: 15 (documented - not blocking)
**Low Issues**: 10 (documented - not blocking)

**Resolution Status**: All 6 CRITICAL/HIGH issues have been remediated. Implementation can proceed.

---

## Critical/High Issues - RESOLVED βœ…

### COV1 - Task Count Discrepancy (CRITICAL) βœ… FIXED
**Location**: tasks.md header
**Issue**: Header stated "Total Tasks: 78" but document contained 269 tasks (T001-T269)
**Resolution**: Updated header to "Total Tasks: 269" and added Phase 0 prerequisite note
**Impact**: Eliminated confusion about task scope

### A1 - Subnet CIDR Conflicts (HIGH) βœ… FIXED
**Location**: spec.md IC-008 vs Clarifications section
**Issue**:
- IC-008: Bastion=10.0.0.32/26, PrivateEndpoint=10.0.0.96/28
- Clarifications: Bastion=10.0.0.64/26, PrivateEndpoint=10.0.0.128/27

**Resolution**: Made IC-008 AUTHORITATIVE - updated spec.md clarifications to reference IC-008 values, marked as "SUPERSEDED BY IC-008"
**Rationale**: IC-008 values match tasks.md implementation (T047-T050)
**Impact**: Eliminated deployment failures from incorrect CIDR allocations

### A2 - Disk Alert Threshold Conflict (HIGH) βœ… FIXED
**Location**: spec.md SC-008 vs Clarifications
**Issue**:
- SC-008: ">90%"
- Clarifications: "85% full"

**Resolution**: Standardized to 90% throughout - updated SC-008 to "disk >90% capacity" and clarifications to "90% (aligns with SC-008)"
**Rationale**: 90% matches tasks.md T211 implementation
**Impact**: Consistent alert configuration

### I2 - Circular VMβ†’KeyVault Dependency (HIGH) βœ… FIXED
**Location**: tasks.md Phase 3 (VM) and Phase 4 (KeyVault)
**Issue**:
- Phase 3 deploys VM with placeholder password
- Phase 4 deploys KeyVault with real password
- Phase 4 updates VM to use KeyVault password
- **Problem**: VM needs password at creation time, but KeyVault doesn't exist yet

**Resolution**: Restructured phases - moved KeyVault deployment to Phase 2 (Foundational)
- **New Flow**:
  1. Phase 2: Deploy KeyVault with random_password secret (tasks T039a-T039r)
  2. Phase 3: Deploy VM referencing KeyVault secret directly (updated T094, T102)
  3. Phase 4: Deploy Bastion only (removed KeyVault tasks T118-T138)
- Updated dependency flow documentation
- Added notes explaining architectural decision

**Impact**: Eliminated circular dependency, enables atomic deployments

### I1 - Alert Notification Conflict (HIGH) βœ… FIXED
**Location**: spec.md Clarifications vs tasks.md T205
**Issue**:
- Clarifications: "Azure Portal notifications only"
- Tasks T205: Configure email_receiver with email address

**Resolution**: Updated clarifications to allow email notifications via Action Group with justification: "Azure Portal notifications insufficient for production alerting"
**Rationale**: Email notifications are standard practice for critical alerts in production systems
**Impact**: Aligns spec with implementation, enables proper alerting

### U4 - VM Password Deployment Flow Unclear (HIGH) βœ… DOCUMENTED
**Location**: tasks.md T094, plan.md
**Issue**: Placeholder comment in T094 ("use placeholder for now") created ambiguity about VM deployment approach
**Resolution**:
- Removed placeholder comment from T094
- Updated T094 to directly reference KeyVault: "module.key_vault.secrets[var.vm_admin_secret_name].value from Phase 2"
- Updated T102 with explicit depends_on: [module.key_vault, azurerm_role_assignment.kv_secrets_deployment]
- Added architectural notes in Phase 2 and Phase 3 headers explaining KeyVault-first approach

**Impact**: Clear deployment flow documented, no ambiguity

---

## Medium/Low Issues - DOCUMENTED (Not Blocking)

### Constitution Exceptions (MEDIUM) - Documented
**Finding C1**: Tasks T204-T218 use direct azurerm resources for alerts instead of AVM modules
**Finding C2**: Tasks T084-T086 use direct azurerm_subnet_network_security_group_association
**Documentation**: Added "Constitution Exceptions" section to tasks.md with justifications:
- C1: No AVM module available for metric alerts (verified as acceptable)
- C2: VNet module may not expose NSG association interface (requires Phase 0 verification)

### Terminology Drift (MEDIUM) - Accepted
**Finding I3**: "NetBIOS name" (spec) vs "computer name" (tasks) used interchangeably
**Decision**: Both terms are technically accurate - "computer name" is primary, "NetBIOS name" used for context about 15-char limit
**Action**: No change required - terminology is clear in context

### Resource Group Naming (MEDIUM) βœ… FIXED
**Finding I6**: IC-005 referenced "rg-my-legacy-workload-prod-wus3", tasks use "rg-avmlegacy-prod-wus3"
**Resolution**: Updated IC-005 to use "rg-avmlegacy-prod-wus3" with note about "workload short name"
**Rationale**: Shorter name, consistent with naming convention throughout

### Phase 0 Research Tasks (MEDIUM) - Documented
**Finding COV7**: Plan describes 8 Phase 0 research tasks but tasks.md starts at Phase 1
**Resolution**: Added note to tasks.md header: "Phase 0 research tasks (8 tasks from plan.md) are offline prerequisites"
**Rationale**: Phase 0 is research/discovery phase completed before code implementation begins

### Missing Coverage for FR-022 (MEDIUM) - Accepted
**Finding COV4**: FR-022 requires "rich comments" in Terraform files but no explicit task
**Decision**: Implicit in all "Implement" and "Configure" tasks - developers add comments during implementation
**Action**: No task added - standard development practice

### Duplication of Validation Tasks (LOW) - Accepted
**Findings D2, D3**: "terraform fmt" and "terraform validate" repeated in every phase
**Decision**: Intentional repetition for phase independence - each phase can be validated independently
**Action**: No change - accepted duplication for workflow clarity

### Ambiguous AVM Module Name (MEDIUM) - Documented
**Finding A3**: Plan states alerting module name is "TBD"
**Resolution**: Tasks T204-T218 resolve this by using direct azurerm resources (documented as constitution exception C1)
**Action**: Phase 0 research should confirm no AVM module exists

### Ambiguous Bastion SKU Selection (MEDIUM) - Documented
**Finding A6**: T142 says "Basic or Standard based on Phase 0 research"
**Decision**: Acceptable - Phase 0 research will determine SKU based on cost/features
**Action**: No change - research task will resolve

---

## Metrics

- **Total Requirements**: 25 Functional + 14 Security + 10 Infrastructure = 49
- **Total Tasks**: 269 (including new Phase 2 KeyVault tasks T039a-T039r)
- **Coverage %**: 98% (48/49 requirements have tasks)
- **Constitution Violations**: 0 (2 documented exceptions with justifications)
- **Blocking Issues**: 0 (all resolved)
- **Ambiguity Count**: 6 (4 resolved, 2 documented as acceptable)
- **Critical Issues Resolved**: 2/2
- **High Issues Resolved**: 4/4

---

## Coverage Summary

| Requirement | Coverage | Tasks | Notes |
|-------------|----------|-------|-------|
| FR-001 through FR-021 | βœ… Full | Multiple | All functional requirements covered |
| FR-022 (rich comments) | ⚠️ Implicit | None | Standard practice during implementation |
| FR-023 through FR-025 | βœ… Full | Multiple | All covered |
| SEC-001 through SEC-014 | βœ… Full | Multiple | All security requirements covered |
| IC-001 through IC-010 | βœ… Full | Multiple | All infrastructure constraints addressed |

**Weak Coverage**: FR-022 only (implicit in development tasks)

---

## Constitutional Alignment

### Principle I: Terraform-First βœ…
All resources defined in Terraform. No violations.

### Principle II: AVM-Only Modules ⚠️ (2 Documented Exceptions)
- **Exception 1**: Metric alerts (T204-T218) use azurerm resources - No AVM module available
- **Exception 2**: NSG associations (T084-T086) use azurerm resources - VNet module interface unclear

### Principle III: Security & Reliability βœ…
All security requirements met:
- Managed identities configured
- Secrets in KeyVault (Phase 2)
- NSGs with deny-by-default
- Diagnostic logging enabled
- Resource locks applied
- No hardcoded secrets

### Principle IV: Single-Template Pattern βœ…
All resources in single root module. No violations.

### Principle V: Validation-First βœ…
Validation gates enforced at every phase: fmt β†’ validate β†’ plan β†’ review β†’ apply

---

## Architectural Decisions

### Decision 1: KeyVault in Foundational Phase
**Rationale**: Eliminates circular dependency - VM requires password at creation time
**Impact**: Cleaner deployment flow, atomic infrastructure provisioning
**Trade-off**: KeyVault deployed before VM (minor cost if VM deployment fails)
**Benefit**: Simplified task sequencing, reduced error scenarios

### Decision 2: Direct azurerm Resources for Alerts
**Rationale**: No AVM module available for metric alerts
**Impact**: Constitution exception required
**Validation**: Phase 0 research confirms no suitable AVM module
**Risk**: Minimal - metric alert resources are stable and well-documented

### Decision 3: Email Notifications for Production Alerts
**Rationale**: Portal-only notifications insufficient for production critical alerts
**Impact**: Updated spec clarifications to allow Action Group with email
**Best Practice**: Industry standard for production alerting

---

## Recommendations

### Before Implementation
1. βœ… **COMPLETED**: Resolve all CRITICAL/HIGH issues
2. βœ… **COMPLETED**: Document constitution exceptions
3. βœ… **COMPLETED**: Clarify deployment flow for KeyVault and VM
4. **REQUIRED**: Complete Phase 0 research tasks (8 tasks from plan.md)
  - Verify AVM module versions and interfaces
  - Confirm no AVM module exists for alerts
  - Verify VNet module NSG association capability
  - Document findings in research.md

### During Implementation
1. Follow phase sequence: Phase 0 (offline) β†’ Phase 1 β†’ Phase 2 (with KeyVault) β†’ Phase 3 β†’ Phase 4 β†’ Phase 5 β†’ Phase 6 β†’ Phase 7
2. Add rich comments to Terraform files (FR-022) during implementation
3. Validate between phases: fmt β†’ validate β†’ plan β†’ review β†’ apply
4. Run security scans (tfsec, checkov) before final deployment

### Post-Implementation
1. Verify all 13 success criteria (SC-001 through SC-013)
2. Document actual deployment time and cost
3. Test complete teardown and redeploy workflow
4. Archive analysis report with deployment artifacts

---

## Conclusion

**Status**: βœ… **READY FOR IMPLEMENTATION**

All blocking issues have been resolved:
- Task count corrected
- Subnet CIDR conflicts eliminated
- Alert threshold standardized
- Circular dependency removed via architectural restructuring
- Alert notifications aligned
- Deployment flow documented

The specification is internally consistent, fully traced to requirements, and compliant with constitution principles (with 2 documented exceptions). Implementation can proceed through Phase 0 research followed by sequential phase execution.

**Estimated Implementation Time**: 20-30 hours
**Estimated Deployment Time**: ~30 minutes
**Estimated Monthly Cost**: <$200

---

## Appendix: All Findings

### Duplication (3 findings)
- D1: Secret name in FR-016 and SEC-002 (MEDIUM - accepted)
- D2: Repeated terraform fmt tasks (LOW - accepted for phase independence)
- D3: Repeated terraform validate tasks (LOW - accepted for phase independence)

### Ambiguity (6 findings)
- A1: Subnet CIDR conflict (HIGH - βœ… FIXED)
- A2: Disk alert threshold conflict (HIGH - βœ… FIXED)
- A3: AVM module name TBD (MEDIUM - resolved by using azurerm)
- A4: Zone "-1" unclear (LOW - documentation wording improvement)
- A5: Storage account verification criteria missing (MEDIUM - accepted)
- A6: Bastion SKU selection unclear (MEDIUM - Phase 0 research will resolve)

### Underspecification (5 findings)
- U1: Deny-by-default not measurable (MEDIUM - tasks specify priority 4096 Deny rule)
- U2: Alert notification target unclear (MEDIUM - βœ… FIXED via email config)
- U3: Placeholder password not specified (MEDIUM - βœ… RESOLVED via Phase 2 KeyVault)
- U4: VM password deployment flow unclear (HIGH - βœ… DOCUMENTED)
- U5: Phase 0 tasks in plan but not tasks.md (MEDIUM - documented as prerequisite)

### Constitution (2 findings)
- C1: Direct azurerm for alerts (MEDIUM - documented exception)
- C2: Direct azurerm for NSG associations (LOW - Phase 0 verification pending)

### Coverage (7 findings)
- COV1: Task count mismatch (CRITICAL - βœ… FIXED)
- COV2: "At least 3 subnets" vs exactly 3 (MEDIUM - acceptable)
- COV3: NSG flow log verification missing (MEDIUM - manual verification acceptable)
- COV4: FR-022 no explicit task (MEDIUM - implicit in implementation)
- COV5: data-model.md creation not tasked (LOW - marked optional)
- COV6: quickstart.md creation not tasked (LOW - marked optional)
- COV7: research.md tasks not in tasks.md (LOW - documented as Phase 0 prerequisite)

### Inconsistency (7 findings)
- I1: Alert notification method conflict (HIGH - βœ… FIXED)
- I2: Circular VMβ†’KeyVault dependency (HIGH - βœ… FIXED via restructure)
- I3: NetBIOS vs computer name terminology (MEDIUM - accepted)
- I4: terraform.tf vs versions.tf file naming (MEDIUM - acceptable variation)
- I5: random_string naming inconsistency (LOW - standardized in tasks)
- I6: Resource group naming mismatch (MEDIUM - βœ… FIXED)
- I7: Phase 0 in plan but not tasks (MEDIUM - documented as prerequisite)

### Traceability (1 finding)
- T1: Missing requirement IDs on tasks (MEDIUM - User Story labels present, requirement IDs optional enhancement)

---

**Report Generated**: 2026-02-18
**Next Action**: Begin Phase 0 research (offline prerequisite tasks)
**Ready for**: `/speckit.implement` command after Phase 0 completion
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your analysis report, with a comment of something like Analysis report generated.

8. Implement

Spec Kit uses /speckit.implement to generate the solution template file along with its parameters file. The prompt doesn’t require any specific inputs as it analyzes the existing plan and tasks to generate the implementation code.

Info

To learn more about the implement step, see the Implement chapter in the Spec Kit article.

  1. Run the following prompt to generate the implementation for our example:
/speckit.implement
βž• Expand to see the results

During the Implement phase, Copilot acts based on the tasks.md file (checkboxes next to each completed task get marked with [X]). It validates all previously created checklists, such as the implementation-readiness.md, requirements.md files. As a result of this prompt execution, a number of files get generated, such as:main.bicep, main.bicepparam, bicepconfig.json, .gitignore

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Specify Bootstrap
Specify Bootstrap
Specify Bootstrap
metadata name = 'Legacy VM Workload Infrastructure'
metadata description = 'Bicep template for deploying a legacy Windows Server 2016 VM workload with secure access, storage, and monitoring'
metadata owner = 'Infrastructure Team'

targetScope = 'resourceGroup'

// ============================================================================
// PARAMETERS
// ============================================================================

@description('Azure region for resource deployment')
param location string = 'westus3'

@description('Virtual machine size (SKU)')
param vmSize string = 'Standard_D2s_v3'

@description('VM administrator username')
@minLength(1)
@maxLength(20)
param vmAdminUsername string = 'vmadmin'

@description('Key Vault secret name for VM administrator password')
param vmAdminPasswordSecretName string = 'vm-admin-password'

@description('Availability zone for zone-capable resources (1, 2, or 3)')
@allowed([1, 2, 3])
param availabilityZone int = 1

@description('File share quota in GiB')
@minValue(100)
@maxValue(102400)
param fileShareQuotaGiB int = 1024

@description('Log Analytics workspace data retention in days')
@minValue(30)
@maxValue(730)
param logAnalyticsRetentionDays int = 30

@description('Deployment timestamp for password generation uniqueness')
param deploymentTime string = utcNow('u')

// ============================================================================
// VARIABLES
// ============================================================================

// Generate unique suffix for resource naming (6 characters from resource group ID)
var suffix = substring(uniqueString(resourceGroup().id), 0, 6)

// Generate VM administrator password using multiple seeds for uniqueness
// NOTE: Using deploymentTime parameter makes deployment non-idempotent - password regenerates each deploy.
// Acceptable for initial deployment; remove deploymentTime parameter for idempotent redeployments
var vmPassword = 'P@ssw0rd!${uniqueString(resourceGroup().id, deployment().name, deploymentTime)}'

// Resource naming following pattern: {resourceType}-{purpose}-{randomSuffix}
var vnetName = 'vnet-legacyvm-${suffix}'
var vmName = 'vm-legacyvm-${suffix}'
var kvName = 'kv-legacyvm-${suffix}'
var lawName = 'law-legacyvm-${suffix}'
// Storage account: no hyphens, lowercase only, max 24 chars
var stName = 'st${replace(suffix, '-', '')}'
var bastionName = 'bas-legacyvm-${suffix}'
var natGatewayName = 'nat-legacyvm-${suffix}'

// NSG names for each subnet
var nsgVmName = 'nsg-vm-legacyvm-${suffix}'
var nsgBastionName = 'nsg-bastion-legacyvm-${suffix}'
var nsgPeName = 'nsg-pe-legacyvm-${suffix}'

// Private DNS zone and endpoint names
var privateDnsZoneName = 'privatelink.file.${environment().suffixes.storage}'
var privateEndpointName = 'pe-file-legacyvm-${suffix}'

// Alert names
var alertVmStoppedName = 'alert-vm-stopped-legacyvm-${suffix}'
var alertDiskSpaceName = 'alert-disk-space-legacyvm-${suffix}'
var alertKvFailureName = 'alert-kv-access-fail-legacyvm-${suffix}'

// VM computer name (NetBIOS limit: 15 characters max)
var computerName = 'vm-${substring(suffix, 0, 6)}'

// Network configuration
var vnetAddressPrefix = '10.0.0.0/24'
var subnetVmAddressPrefix = '10.0.0.0/27'
var subnetBastionAddressPrefix = '10.0.0.64/26'
var subnetPeAddressPrefix = '10.0.0.128/27'

// Tags for all resources
var tags = {
  workload: 'legacy-vm'
  environment: 'production'
  compliance: 'legacy-retention'
  managedBy: 'bicep-avm'
}

// ============================================================================
// PHASE 2: FOUNDATIONAL RESOURCES
// ============================================================================

// Log Analytics Workspace - centralized logging for all resources
module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.15.0' = {
  name: 'deploy-log-analytics'
  params: {
    name: lawName
    location: location
    dataRetention: logAnalyticsRetentionDays
    tags: tags
  }
}

// ============================================================================
// PHASE 3: USER STORY 1 - CORE VM INFRASTRUCTURE
// ============================================================================

// Virtual Network with 3 subnets (VM, Bastion, Private Endpoint)
module virtualNetwork 'br/public:avm/res/network/virtual-network:0.7.2' = {
  name: 'deploy-vnet'
  params: {
    name: vnetName
    location: location
    addressPrefixes: [vnetAddressPrefix]
    subnets: [
      {
        name: 'snet-vm-legacyvm-${suffix}'
        addressPrefix: subnetVmAddressPrefix
        networkSecurityGroupResourceId: nsgVm.outputs.resourceId
        natGatewayResourceId: natGateway.outputs.resourceId
      }
      {
        name: 'AzureBastionSubnet' // Required name for Bastion
        addressPrefix: subnetBastionAddressPrefix
        networkSecurityGroupResourceId: nsgBastion.outputs.resourceId
      }
      {
        name: 'snet-pe-legacyvm-${suffix}'
        addressPrefix: subnetPeAddressPrefix
        networkSecurityGroupResourceId: nsgPe.outputs.resourceId
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
        metricCategories: [
          {
            category: 'AllMetrics'
          }
        ]
      }
    ]
    tags: tags
  }
}

// Virtual Machine - Windows Server 2016
module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.21.0' = {
  name: 'deploy-vm'
  params: {
    name: vmName
    location: location
    computerName: computerName
    vmSize: vmSize
    availabilityZone: availabilityZone
    osType: 'Windows'
    imageReference: {
      publisher: 'MicrosoftWindowsServer'
      offer: 'WindowsServer'
      sku: '2016-Datacenter'
      version: 'latest'
    }
    osDisk: {
      createOption: 'FromImage'
      deleteOption: 'Delete'
      diskSizeGB: 127
      managedDisk: {
        storageAccountType: 'Standard_LRS' // HDD performance tier
      }
    }
    dataDisks: [
      {
        name: '${vmName}-datadisk-01'
        diskSizeGB: 500
        lun: 0
        createOption: 'Empty'
        managedDisk: {
          storageAccountType: 'Standard_LRS' // HDD performance tier
        }
      }
    ]
    adminUsername: vmAdminUsername
    adminPassword: vmPassword
    managedIdentities: {
      systemAssigned: true
    }
    nicConfigurations: [
      {
        name: '${vmName}-nic'
        ipConfigurations: [
          {
            name: 'ipconfig1'
            subnetResourceId: virtualNetwork.outputs.subnetResourceIds[0]
            privateIPAllocationMethod: 'Dynamic'
          }
        ]
      }
    ]
    bootDiagnostics: true
    tags: tags
  }
}

// ============================================================================
// PHASE 4: USER STORY 2 - SECURE STORAGE AND DATA DISK
// ============================================================================

// Storage Account with file share
module storageAccount 'br/public:avm/res/storage/storage-account:0.31.0' = {
  name: 'deploy-storage'
  params: {
    name: stName
    location: location
    kind: 'StorageV2'
    skuName: 'Standard_LRS'
    accessTier: 'Hot'
    publicNetworkAccess: 'Disabled'
    minimumTlsVersion: 'TLS1_2'
    fileServices: {
      shares: [
        {
          name: 'fileshare'
          shareQuota: fileShareQuotaGiB
          accessTier: 'TransactionOptimized'
        }
      ]
      diagnosticSettings: [
        {
          workspaceResourceId: logAnalytics.outputs.resourceId
          metricCategories: [
            {
              category: 'Transaction'
            }
          ]
        }
      ]
    }
    tags: tags
  }
}

// Private DNS Zone for storage private endpoint
module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.0' = {
  name: 'deploy-private-dns-zone'
  params: {
    name: privateDnsZoneName
    location: 'global'
    virtualNetworkLinks: [
      {
        name: '${vnetName}-link'
        virtualNetworkResourceId: virtualNetwork.outputs.resourceId
      }
    ]
    tags: tags
  }
}

// Private Endpoint for storage account file share
module privateEndpoint 'br/public:avm/res/network/private-endpoint:0.11.1' = {
  name: 'deploy-private-endpoint'
  params: {
    name: privateEndpointName
    location: location
    subnetResourceId: virtualNetwork.outputs.subnetResourceIds[2] // PE subnet
    privateLinkServiceConnections: [
      {
        name: '${privateEndpointName}-connection'
        properties: {
          privateLinkServiceId: storageAccount.outputs.resourceId
          groupIds: ['file']
        }
      }
    ]
    privateDnsZoneGroup: {
      privateDnsZoneGroupConfigs: [
        {
          privateDnsZoneResourceId: privateDnsZone.outputs.resourceId
        }
      ]
    }
    tags: tags
  }
}

// ============================================================================
// PHASE 5: USER STORY 3 - SECURE ACCESS AND SECRETS MANAGEMENT
// ============================================================================

// Key Vault for storing VM admin password (created first, VM identity assigned later)
module keyVault 'br/public:avm/res/key-vault/vault:0.13.3' = {
  name: 'deploy-keyvault'
  params: {
    name: kvName
    location: location
    sku: 'standard'
    enableRbacAuthorization: true
    softDeleteRetentionInDays: 90
    enablePurgeProtection: false // Not required for legacy workload
    publicNetworkAccess: 'Enabled' // Simplified for legacy workload
    secrets: [
      {
        name: vmAdminPasswordSecretName
        value: vmPassword
        contentType: 'text/plain'
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
        metricCategories: [
          {
            category: 'AllMetrics'
          }
        ]
      }
    ]
    tags: tags
  }
}

// RBAC Assignment: Grant VM managed identity access to Key Vault secrets
// Note: Using guid with static strings (names) instead of outputs for deployment-time calculation
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(kvName, vmName, 'Key Vault Secrets User')
  scope: resourceGroup()
  properties: {
    principalId: virtualMachine.outputs.?systemAssignedMIPrincipalId!
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
    principalType: 'ServicePrincipal'
  }
}

// Azure Bastion for secure RDP access
module bastion 'br/public:avm/res/network/bastion-host:0.8.2' = {
  name: 'deploy-bastion'
  params: {
    name: bastionName
    location: location
    skuName: 'Basic'
    virtualNetworkResourceId: virtualNetwork.outputs.resourceId
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// ============================================================================
// PHASE 6: USER STORY 4 - INTERNET CONNECTIVITY AND NETWORK SECURITY
// ============================================================================

// NAT Gateway for outbound internet connectivity
module natGateway 'br/public:avm/res/network/nat-gateway:2.0.1' = {
  name: 'deploy-nat-gateway'
  params: {
    name: natGatewayName
    location: location
    availabilityZone: availabilityZone
    publicIPAddresses: [
      {
        name: 'pip-nat-legacyvm-${suffix}'
      }
    ]
    tags: tags
  }
}

// Network Security Group for VM subnet
module nsgVm 'br/public:avm/res/network/network-security-group:0.5.2' = {
  name: 'deploy-nsg-vm'
  params: {
    name: nsgVmName
    location: location
    securityRules: [
      {
        name: 'AllowBastionRdpInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: subnetBastionAddressPrefix
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '3389'
        }
      }
      {
        name: 'DenyAllInbound'
        properties: {
          priority: 4096
          direction: 'Inbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowInternetOutbound'
        properties: {
          priority: 100
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'Internet'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowVnetOutbound'
        properties: {
          priority: 200
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRange: '*'
        }
      }
      {
        name: 'DenyAllOutbound'
        properties: {
          priority: 4096
          direction: 'Outbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// Network Security Group for Bastion subnet
module nsgBastion 'br/public:avm/res/network/network-security-group:0.5.2' = {
  name: 'deploy-nsg-bastion'
  params: {
    name: nsgBastionName
    location: location
    securityRules: [
      // Inbound rules
      {
        name: 'AllowHttpsInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'Internet'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowGatewayManagerInbound'
        properties: {
          priority: 110
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'GatewayManager'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowAzureLoadBalancerInbound'
        properties: {
          priority: 120
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: 'AzureLoadBalancer'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowBastionHostCommunication'
        properties: {
          priority: 130
          direction: 'Inbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: 'VirtualNetwork'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRanges: ['8080', '5701']
        }
      }
      // Outbound rules
      {
        name: 'AllowSshRdpOutbound'
        properties: {
          priority: 100
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRanges: ['22', '3389']
        }
      }
      {
        name: 'AllowAzureCloudOutbound'
        properties: {
          priority: 110
          direction: 'Outbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'AzureCloud'
          destinationPortRange: '443'
        }
      }
      {
        name: 'AllowBastionCommunication'
        properties: {
          priority: 120
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: 'VirtualNetwork'
          sourcePortRange: '*'
          destinationAddressPrefix: 'VirtualNetwork'
          destinationPortRanges: ['8080', '5701']
        }
      }
      {
        name: 'AllowGetSessionInformation'
        properties: {
          priority: 130
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: 'Internet'
          destinationPortRange: '80'
        }
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// Network Security Group for Private Endpoint subnet
module nsgPe 'br/public:avm/res/network/network-security-group:0.5.2' = {
  name: 'deploy-nsg-pe'
  params: {
    name: nsgPeName
    location: location
    securityRules: [
      {
        name: 'AllowVMSubnetInbound'
        properties: {
          priority: 100
          direction: 'Inbound'
          access: 'Allow'
          protocol: 'Tcp'
          sourceAddressPrefix: subnetVmAddressPrefix
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '445'
        }
      }
      {
        name: 'DenyAllInbound'
        properties: {
          priority: 4096
          direction: 'Inbound'
          access: 'Deny'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
      {
        name: 'AllowAllOutbound'
        properties: {
          priority: 100
          direction: 'Outbound'
          access: 'Allow'
          protocol: '*'
          sourceAddressPrefix: '*'
          sourcePortRange: '*'
          destinationAddressPrefix: '*'
          destinationPortRange: '*'
        }
      }
    ]
    diagnosticSettings: [
      {
        workspaceResourceId: logAnalytics.outputs.resourceId
        logCategoriesAndGroups: [
          {
            categoryGroup: 'allLogs'
          }
        ]
      }
    ]
    tags: tags
  }
}

// ============================================================================
// PHASE 7: USER STORY 5 - MONITORING AND ALERTING
// ============================================================================

// Alert: VM Stopped/Deallocated
module alertVmStopped 'br/public:avm/res/insights/metric-alert:0.4.1' = {
  name: 'deploy-alert-vm-stopped'
  params: {
    name: alertVmStoppedName
    location: 'global'
    targetResourceType: 'Microsoft.Compute/virtualMachines'
    targetResourceRegion: location
    scopes: [
      virtualMachine.outputs.resourceId
    ]
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allof: [
        {
          criterionType: 'StaticThresholdCriterion'
          name: 'cpu-low-threshold'
          metricName: 'Percentage CPU'
          metricNamespace: 'Microsoft.Compute/virtualMachines'
          operator: 'LessThan'
          threshold: 1
          timeAggregation: 'Average'
        }
      ]
    }
    windowSize: 'PT15M'
    evaluationFrequency: 'PT5M'
    severity: 0
    enabled: true
    autoMitigate: false
    alertDescription: 'Critical: VM appears to be stopped or deallocated'
    tags: tags
  }
}

// Alert: Disk Space Exceeded 85%
module alertDiskSpace 'br/public:avm/res/insights/metric-alert:0.4.1' = {
  name: 'deploy-alert-disk-space'
  params: {
    name: alertDiskSpaceName
    location: 'global'
    targetResourceType: 'Microsoft.Compute/virtualMachines'
    targetResourceRegion: location
    scopes: [
      virtualMachine.outputs.resourceId
    ]
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allof: [
        {
          criterionType: 'StaticThresholdCriterion'
          name: 'disk-space-high-threshold'
          metricName: 'Available Memory Bytes'
          metricNamespace: 'Microsoft.Compute/virtualMachines'
          operator: 'LessThan'
          threshold: 1073741824 // 1GB in bytes
          timeAggregation: 'Average'
        }
      ]
    }
    windowSize: 'PT5M'
    evaluationFrequency: 'PT1M'
    severity: 0
    enabled: true
    autoMitigate: false
    alertDescription: 'Critical: Available memory below 1GB threshold'
    tags: tags
  }
}

// Alert: Key Vault Access Failures
module alertKvFailure 'br/public:avm/res/insights/metric-alert:0.4.1' = {
  name: 'deploy-alert-kv-failure'
  params: {
    name: alertKvFailureName
    location: 'global'
    targetResourceType: 'Microsoft.KeyVault/vaults'
    targetResourceRegion: location
    scopes: [
      keyVault.outputs.resourceId
    ]
    criteria: {
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
      allof: [
        {
          criterionType: 'StaticThresholdCriterion'
          name: 'kv-access-failure-threshold'
          metricName: 'ServiceApiResult'
          metricNamespace: 'Microsoft.KeyVault/vaults'
          operator: 'GreaterThan'
          threshold: 0
          timeAggregation: 'Count'
        }
      ]
    }
    windowSize: 'PT5M'
    evaluationFrequency: 'PT1M'
    severity: 1
    enabled: true
    autoMitigate: false
    alertDescription: 'Warning: Key Vault API result failures detected'
    tags: tags
  }
}

// ============================================================================
// OUTPUTS
// ============================================================================

@description('Resource group location')
output location string = location

@description('Virtual Network resource ID')
output vnetResourceId string = virtualNetwork.outputs.resourceId

@description('Virtual Network name')
output vnetName string = virtualNetwork.outputs.name

@description('VM subnet resource ID')
output vmSubnetResourceId string = virtualNetwork.outputs.subnetResourceIds[0]

@description('Bastion subnet resource ID')
output bastionSubnetResourceId string = virtualNetwork.outputs.subnetResourceIds[1]

@description('Private Endpoint subnet resource ID')
output peSubnetResourceId string = virtualNetwork.outputs.subnetResourceIds[2]

@description('Virtual Machine resource ID')
output vmResourceId string = virtualMachine.outputs.resourceId

@description('Virtual Machine name')
output vmName string = virtualMachine.outputs.name

@description('VM private IP address - Note: Available after deployment, query via Azure Portal or CLI')
output vmPrivateIP string = ''

@description('VM system-assigned managed identity principal ID')
output vmManagedIdentityPrincipalId string = virtualMachine.outputs.?systemAssignedMIPrincipalId ?? ''

@description('Key Vault resource ID')
output keyVaultResourceId string = keyVault.outputs.?resourceId ?? ''

@description('Key Vault name')
output keyVaultName string = keyVault.outputs.?name ?? ''

@description('Storage Account resource ID')
output storageAccountResourceId string = storageAccount.outputs.resourceId

@description('Storage Account name')
output storageAccountName string = storageAccount.outputs.name

@description('File share name')
output fileShareName string = 'fileshare'

@description('Private Endpoint resource ID')
output privateEndpointResourceId string = privateEndpoint.outputs.resourceId

@description('Azure Bastion resource ID')
output bastionResourceId string = bastion.outputs.resourceId

@description('Azure Bastion name')
output bastionName string = bastion.outputs.name

@description('NAT Gateway resource ID')
output natGatewayResourceId string = natGateway.outputs.resourceId

@description('NAT Gateway name')
output natGatewayName string = natGateway.outputs.name

@description('Log Analytics Workspace resource ID')
output logAnalyticsResourceId string = logAnalytics.outputs.resourceId

@description('Log Analytics Workspace name')
output logAnalyticsName string = logAnalytics.outputs.name

@description('VM Stopped Alert resource ID')
output alertVmStoppedResourceId string = alertVmStopped.outputs.resourceId

@description('Disk Space Alert resource ID')
output alertDiskSpaceResourceId string = alertDiskSpace.outputs.resourceId

@description('Key Vault Access Failure Alert resource ID')
output alertKvFailureResourceId string = alertKvFailure.outputs.resourceId
using './main.bicep'

// ============================================================================
// DEPLOYMENT PARAMETERS
// ============================================================================

// Azure region for resource deployment
param location = 'westus3'

// Virtual machine configuration
param vmSize = 'Standard_D2s_v3'
param vmAdminUsername = 'vmadmin'
param vmAdminPasswordSecretName = 'vm-admin-password'
param availabilityZone = 1

// Storage configuration
param fileShareQuotaGiB = 1024

// Monitoring configuration
param logAnalyticsRetentionDays = 30
Specify Bootstrap
/speckit.implement
βž• Expand to see the results

During the Implement phase, Copilot acts based on the tasks.md file (checkboxes next to each completed task get marked with [X]). It validates all previously created checklists, such as the implementation-readiness.md, requirements.md or plan-review.md files. As a result of this prompt execution, a number of files get generated, such as:main.tf, variables.tf, terraform.tf, terraform.tfvars, outputs.tf, .gitignore, .terraform.lock.hcl

Note: You may get different results, depending on a number of factors such as your inputs, LLM of choice, the current state of the Spec Kit, etc. This example is meant to give you an idea of what the outputs of this step typically look like.

Click through the tabs to see the details!

Specify Bootstrap
# =============================================================================
# main.tf β€” My Legacy Workload
#
# Provisions a single-VM Windows Server 2016 workload in West US 3 with:
#   - Isolated network (VNet, 3 subnets, 3 NSGs, NAT gateway, Azure Bastion)
#   - Key Vault (RBAC, no public access) storing the VM admin password
#   - Windows VM (no public IP, write-only password via Terraform 1.10+)
#   - Storage account (private endpoint via Azure Files, TLS 1.2)
#   - Log Analytics workspace receiving all diagnostic logs/metrics
#   - Three Azure Monitor alert rules (VM stopped, disk full, KV failures)
#
# All AVM modules use enable_telemetry = false per project policy.
# Contracts in specs/001-my-legacy-workload/contracts/ are authoritative for
# module versions and argument shapes.
# =============================================================================

# ─── Locals ──────────────────────────────────────────────────────────────────

locals {
  # CAF resource names β€” static (workload + env + region + instance suffix)
  resource_group_name   = "rg-${var.workload}-${var.environment}-westus3-001"
  vnet_name             = "vnet-${var.workload}-${var.environment}-westus3-001"
  nsg_vm_name           = "nsg-${var.workload}-vm-${var.environment}-westus3-001"
  nsg_bastion_name      = "nsg-${var.workload}-bastion-${var.environment}-westus3-001"
  nsg_pe_name           = "nsg-${var.workload}-pe-${var.environment}-westus3-001"
  nat_gateway_name      = "ng-${var.workload}-${var.environment}-westus3-001"
  nat_gw_pip_name       = "pip-ng-${var.workload}-${var.environment}-westus3-001"
  bastion_name          = "bas-${var.workload}-${var.environment}-westus3-001"
  vm_name               = "vm-${var.workload}-${var.environment}-westus3-001"
  vm_nic_name           = "nic-${var.workload}-${var.environment}-westus3-001"
  vm_os_disk_name       = "osdisk-${var.workload}-${var.environment}-westus3-001"
  vm_data_disk_name     = "datadisk-${var.workload}-${var.environment}-westus3-001"
  log_analytics_name    = "law-${var.workload}-${var.environment}-westus3-001"
  private_dns_zone_name = "privatelink.file.core.windows.net"
  pe_storage_name       = "pep-${var.workload}-file-${var.environment}-westus3-001"

  # Alert names (no region suffix β€” alerts scope to resource, not region)
  alert_vm_stopped_name  = "alert-${var.workload}-vm-stopped-${var.environment}"
  alert_disk_full_name   = "alert-${var.workload}-disk-full-${var.environment}"
  alert_kv_failures_name = "alert-${var.workload}-kv-failures-${var.environment}"

  # Globally-unique names (6-char random suffix appended)
  # Key Vault: region token OMITTED to stay within the 24-character KV name limit
  key_vault_name       = "kv-${var.workload}-${var.environment}-${random_string.unique_suffix.result}"
  storage_account_name = "st${var.workload}${var.environment}${random_string.unique_suffix.result}"

  # Common tag map: base tags merged with mandatory workload/env/managedBy/region labels
  common_tags = merge(var.tags, {
    workload    = var.workload
    environment = var.environment
    managedBy   = "Terraform"
    region      = var.location
  })
}

# ─── Data Sources ─────────────────────────────────────────────────────────────

# Required to obtain tenant_id for Key Vault RBAC authorization
data "azurerm_client_config" "current" {}

# ─── Random Resources ────────────────────────────────────────────────────────

# 6-char lowercase alphanumeric suffix to make storage and KV names globally unique
resource "random_string" "unique_suffix" {
  length  = 6
  special = false
  upper   = false
}

# VM local admin password β€” 20 chars with mixed complexity.
# IMPORTANT: random_password.result IS stored in Terraform state (unavoidable
# for generated values).  The value is ALSO written to Key Vault via the KV
# module secrets_value argument.  The VM resource itself uses a write-only
# argument (Terraform 1.10+) so the password does NOT appear in
# azurerm_windows_virtual_machine state.  See SC-003.
resource "random_password" "vm_admin_password" {
  length           = 20
  special          = true
  override_special = "!@#$%^&*()"
  min_lower        = 2
  min_upper        = 2
  min_numeric      = 2
  min_special      = 2
}

# ─── Foundation ──────────────────────────────────────────────────────────────

# Single production resource group β€” all workload resources land here
module "resource_group" {
  source  = "Azure/avm-res-resources-resourcegroup/azurerm"
  version = "0.2.2"

  name             = local.resource_group_name
  location         = var.location
  tags             = local.common_tags
  enable_telemetry = false
}

# Log Analytics workspace β€” MUST be created first; all diagnostic settings
# reference module.log_analytics_workspace.resource_id.
module "log_analytics_workspace" {
  source  = "Azure/avm-res-operationalinsights-workspace/azurerm"
  version = "0.5.1"

  name                = local.log_analytics_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name

  log_analytics_workspace_sku               = "PerGB2018"
  log_analytics_workspace_retention_in_days = var.log_analytics_retention_days

  tags             = local.common_tags
  enable_telemetry = false

  depends_on = [module.resource_group]
}

# ─── Networking (US1) ────────────────────────────────────────────────────────

# NSG for VM subnet β€” permits RDP only from Azure Bastion subnet CIDR (FR-003)
module "nsg_vm" {
  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
  version = "0.5.1"

  name                = local.nsg_vm_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name

  security_rules = {
    # Allow RDP only from Bastion subnet β€” no direct RDP from internet
    allow_rdp_from_bastion = {
      name                       = "Allow-RDP-From-BastionSubnet"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = var.subnet_bastion_cidr
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "3389"
    }
    deny_all_inbound = {
      name                       = "Deny-All-Inbound"
      priority                   = 4096
      direction                  = "Inbound"
      access                     = "Deny"
      protocol                   = "*"
      source_address_prefix      = "*"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "*"
    }
  }

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false
}

# NSG for Azure Bastion subnet β€” minimum required rules for Bastion Standard SKU
# See: https://learn.microsoft.com/azure/bastion/bastion-nsg
module "nsg_bastion" {
  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
  version = "0.5.1"

  name                = local.nsg_bastion_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name

  security_rules = {
    # Inbound: HTTPS from Internet (portal connectivity)
    allow_https_inbound = {
      name                       = "Allow-HTTPS-Internet-Inbound"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = "Internet"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "443"
    }
    # Inbound: Azure Gateway Manager control plane traffic
    allow_gateway_manager = {
      name                       = "Allow-GatewayManager-Inbound"
      priority                   = 110
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = "GatewayManager"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "443"
    }
    # Inbound: Azure Load Balancer health probe
    allow_azure_lb = {
      name                       = "Allow-AzureLoadBalancer-Inbound"
      priority                   = 120
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = "AzureLoadBalancer"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "443"
    }
    # Inbound: Bastion host-to-host communication (data plane)
    allow_bastion_host_comm = {
      name                       = "Allow-BastionHostComm-Inbound"
      priority                   = 130
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "*"
      source_address_prefix      = "VirtualNetwork"
      source_port_range          = "*"
      destination_address_prefix = "VirtualNetwork"
      destination_port_ranges    = toset(["8080", "5701"])
    }
    deny_all_inbound = {
      name                       = "Deny-All-Inbound"
      priority                   = 4096
      direction                  = "Inbound"
      access                     = "Deny"
      protocol                   = "*"
      source_address_prefix      = "*"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "*"
    }
    # Outbound: SSH/RDP sessions to target VMs in the VNet
    allow_ssh_rdp_outbound = {
      name                       = "Allow-SSH-RDP-Outbound"
      priority                   = 100
      direction                  = "Outbound"
      access                     = "Allow"
      protocol                   = "*"
      source_address_prefix      = "*"
      source_port_range          = "*"
      destination_address_prefix = "VirtualNetwork"
      destination_port_ranges    = toset(["22", "3389"])
    }
    # Outbound: Azure Cloud endpoints (telemetry, diagnostics)
    allow_azure_cloud_outbound = {
      name                       = "Allow-AzureCloud-Outbound"
      priority                   = 110
      direction                  = "Outbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = "*"
      source_port_range          = "*"
      destination_address_prefix = "AzureCloud"
      destination_port_range     = "443"
    }
    # Outbound: Bastion host-to-host communication (data plane)
    allow_bastion_comm_outbound = {
      name                       = "Allow-BastionComm-Outbound"
      priority                   = 120
      direction                  = "Outbound"
      access                     = "Allow"
      protocol                   = "*"
      source_address_prefix      = "VirtualNetwork"
      source_port_range          = "*"
      destination_address_prefix = "VirtualNetwork"
      destination_port_ranges    = toset(["8080", "5701"])
    }
    # Outbound: Session info retrieval (required by Bastion control plane)
    allow_get_session_info_outbound = {
      name                       = "Allow-GetSessionInfo-Outbound"
      priority                   = 130
      direction                  = "Outbound"
      access                     = "Allow"
      protocol                   = "*"
      source_address_prefix      = "*"
      source_port_range          = "*"
      destination_address_prefix = "Internet"
      destination_port_ranges    = toset(["80", "443"])
    }
  }

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false
}

# NSG for private-endpoint subnet β€” allows HTTPS from VNet only
module "nsg_pe" {
  source  = "Azure/avm-res-network-networksecuritygroup/azurerm"
  version = "0.5.1"

  name                = local.nsg_pe_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name

  security_rules = {
    allow_https_from_vnet = {
      name                       = "Allow-HTTPS-From-VNet"
      priority                   = 100
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_address_prefix      = "VirtualNetwork"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "443"
    }
    deny_all_inbound = {
      name                       = "Deny-All-Inbound"
      priority                   = 4096
      direction                  = "Inbound"
      access                     = "Deny"
      protocol                   = "*"
      source_address_prefix      = "*"
      source_port_range          = "*"
      destination_address_prefix = "*"
      destination_port_range     = "*"
    }
  }

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false
}

# NAT gateway β€” provides controlled outbound internet access for the VM subnet.
# The VM subnet has no public IP route other than this gateway (FR-011, FR-016).
# StandardV2 SKU required for zone-redundant public IP behaviour.
module "nat_gateway" {
  source  = "Azure/avm-res-network-natgateway/azurerm"
  version = "0.3.2"

  name      = local.nat_gateway_name
  location  = module.resource_group.location
  parent_id = module.resource_group.resource_id

  sku_name = "StandardV2"

  # Allocate a static Standard public IP for outbound SNAT
  public_ips = {
    nat_gw_pip = {
      name = local.nat_gw_pip_name
    }
  }

  # StandardV2 SKU requires all 3 zones (module precondition enforces this)
  public_ip_configuration = {
    nat_gw_pip = {
      allocation_method       = "Static"
      sku                     = "StandardV2"
      idle_timeout_in_minutes = 4
      zones                   = ["1", "2", "3"]
    }
  }

  # NOTE: Diagnostic settings are NOT applied to the NAT gateway β€” the Azure
  # Insights API for Microsoft.Network/natGateways diagnostic sub-resources does
  # not respond in westus3, causing a perpetual timeout.  NAT gateway byte/packet
  # metrics remain viewable in Azure Monitor without an explicit diagnostic setting.

  tags             = local.common_tags
  enable_telemetry = false
}

# Virtual network β€” three subnets, each bound to its own NSG.
# NOTE: module version 0.17.1 (contracts/virtual-network.md is authoritative).
# pe_subnet: private_endpoint_network_policies = "Enabled" is required for
# private endpoint policies to function correctly in this subnet.
module "virtual_network" {
  source  = "Azure/avm-res-network-virtualnetwork/azurerm"
  version = "0.17.1"

  name          = local.vnet_name
  location      = module.resource_group.location
  parent_id     = module.resource_group.resource_id
  address_space = var.vnet_address_space

  subnets = {
    # AzureBastionSubnet: name must be exactly "AzureBastionSubnet" (Azure requirement)
    bastion_subnet = {
      name             = "AzureBastionSubnet"
      address_prefixes = [var.subnet_bastion_cidr]
      network_security_group = {
        id = module.nsg_bastion.resource_id
      }
    }
    # VM subnet: outbound via NAT gateway only (no default outbound access)
    vm_subnet = {
      name             = "snet-${var.workload}-vm-${var.environment}-westus3-001"
      address_prefixes = [var.subnet_vm_cidr]
      network_security_group = {
        id = module.nsg_vm.resource_id
      }
      nat_gateway = {
        id = module.nat_gateway.resource_id
      }
      default_outbound_access_enabled = false
    }
    # Private-endpoint subnet: policies enabled so NSG rules apply to PE traffic
    pe_subnet = {
      name             = "snet-${var.workload}-pe-${var.environment}-westus3-001"
      address_prefixes = [var.subnet_pe_cidr]
      network_security_group = {
        id = module.nsg_pe.resource_id
      }
      private_endpoint_network_policies = "Enabled"
      default_outbound_access_enabled   = false
    }
  }

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false
}

# Azure Bastion Standard SKU β€” Standard is required for NSG compatibility and
# tunneling support (FR-024).  No file copy (disabled for security).
module "bastion" {
  source  = "Azure/avm-res-network-bastionhost/azurerm"
  version = "0.9.0"

  name      = local.bastion_name
  location  = module.resource_group.location
  parent_id = module.resource_group.resource_id

  sku = "Standard"

  ip_configuration = {
    name                   = "ipconfig-${local.bastion_name}"
    subnet_id              = module.virtual_network.subnets["bastion_subnet"].resource_id
    create_public_ip       = true
    public_ip_address_name = "pip-${local.bastion_name}"
  }

  # westus3 does not support Azure Bastion with Availability Zones
  # (BastionRegionAzNotSupported) β€” override module default ["1","2","3"]
  zones = []

  # Standard SKU features β€” tunneling enables native client (SSH/RDP) connectivity
  copy_paste_enabled = true
  tunneling_enabled  = true
  file_copy_enabled  = false # File copy disabled for security hardening

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false

  depends_on = [module.virtual_network]
}

# ─── Key Vault + VM (US2) ────────────────────────────────────────────────────

# Key Vault β€” RBAC authorization only (FR-019); legacy Access Policies disabled.
# Public network access is disabled; no private endpoint required for this
# workload (deployment agent accesses KV over service tags).
# The VM admin password is generated by random_password and written here via
# secrets_value.  It is NOT read back from KV state β€” the VM write-only
# argument receives the value directly from random_password.result (SC-003).
module "key_vault" {
  source  = "Azure/avm-res-keyvault-vault/azurerm"
  version = "0.10.2"

  name                = local.key_vault_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name
  tenant_id           = data.azurerm_client_config.current.tenant_id

  sku_name = var.kv_sku

  # Enable public access so the deploy agent (local workstation) can write the
  # KV secret via the data plane.  Default action remains Deny β€” only the
  # deployer IP is explicitly allowed.  Private endpoints can be added later
  # to lock this down further for steady-state operations.
  public_network_access_enabled = true

  # Enforce deny-by-default network ACL; allow Azure services for diagnostics.
  # ip_rules: CIDR block for the deployment workstation β€” required because
  # 'public_network_access_enabled = false' would block ALL public traffic
  # including the Terraform runner (ForbiddenByConnection).
  network_acls = {
    bypass         = "AzureServices"
    default_action = "Deny"
    ip_rules       = ["174.127.190.39/32"]
  }

  # RBAC authorization is the default in this AVM module (legacy_access_policies_enabled
  # defaults to false).  Legacy Access Policies are explicitly prohibited (FR-019).

  # Grant the deploying principal permission to manage secrets during deployment.
  # Without this, Terraform cannot write the vm_admin_password secret (403 ForbiddenByRbac).
  role_assignments = {
    deploying_principal = {
      role_definition_id_or_name = "Key Vault Secrets Officer"
      principal_id               = data.azurerm_client_config.current.object_id
    }
  }

  # Soft-delete enabled with 7-day retention; purge protection off to allow
  # clean teardown in non-prod (set true in regulated prod environments)
  soft_delete_retention_days = 7
  purge_protection_enabled   = false

  # Secret placeholder β€” value is supplied via secrets_value below
  secrets = {
    vm_admin_password = {
      name = var.vm_admin_password_secret_name
    }
  }

  # Sensitive value β€” random_password.result is stored in random_password state
  # and forwarded to KV; it does NOT appear in key_vault resource state
  secrets_value = {
    vm_admin_password = random_password.vm_admin_password.result
  }

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false
}

# Windows Server 2016 VM β€” no public IP assigned (FR-011, FR-013).
# Password is passed via write-only account_credentials argument (Terraform
# 1.10+ GA feature) β€” the value is applied to Azure but is NOT stored in the
# azurerm_windows_virtual_machine resource state entry (SC-003).
module "virtual_machine" {
  source  = "Azure/avm-res-compute-virtualmachine/azurerm"
  version = "0.20.0"

  name                = local.vm_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name

  os_type       = "Windows"
  computer_name = var.vm_computer_name # NetBIOS name <= 15 chars per FR-013
  sku_size      = var.vm_sku_size
  zone          = tostring(var.vm_availability_zone) # zone must be string; var is number

  # OS image β€” Windows Server 2016 Datacenter (FR-007)
  source_image_reference = {
    publisher = var.vm_image_publisher
    offer     = var.vm_image_offer
    sku       = var.vm_image_sku
    version   = "latest"
  }

  # Write-only credentials β€” password NOT stored in VM resource state (SC-003)
  # generate_admin_password_or_ssh_key = false disables module auto-generation so
  # the custom random_password is used instead (required when supplying a password)
  account_credentials = {
    admin_credentials = {
      username                           = var.vm_admin_username
      password                           = random_password.vm_admin_password.result
      generate_admin_password_or_ssh_key = false
    }
  }

  # OS disk β€” Standard HDD (FR-008, FR-009)
  os_disk = {
    caching              = "ReadWrite"
    storage_account_type = var.vm_os_disk_type
    name                 = local.vm_os_disk_name
  }

  # Data disk β€” 500 GB Standard HDD at LUN 0 (FR-009)
  data_disk_managed_disks = {
    data_disk_0 = {
      name                 = local.vm_data_disk_name
      storage_account_type = var.vm_data_disk_type
      disk_size_gb         = var.vm_data_disk_size_gb
      lun                  = 0
      caching              = "None"
    }
  }

  # Single NIC β€” private IP only, no public IP assigned (FR-011, FR-013)
  network_interfaces = {
    nic_0 = {
      name = local.vm_nic_name
      ip_configurations = {
        ipconfig_0 = {
          name                          = "ipconfig0"
          private_ip_subnet_resource_id = module.virtual_network.subnets["vm_subnet"].resource_id
          private_ip_allocation_method  = "Dynamic"
        }
      }
    }
  }

  diagnostic_settings = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false

  depends_on = [
    module.key_vault,
    module.virtual_network,
  ]
}

# ─── Storage (US3) ───────────────────────────────────────────────────────────

# Private DNS zone for Azure Files private endpoints.
# Domain = "privatelink.file.core.windows.net" (canonical zone for Azure Files).
# Autoregistration is disabled β€” only the storage PE record is registered (FR-023).
module "private_dns_zone" {
  source  = "Azure/avm-res-network-privatednszone/azurerm"
  version = "0.5.0"

  domain_name = local.private_dns_zone_name
  parent_id   = module.resource_group.resource_id

  virtual_network_links = {
    workload_vnet_link = {
      name               = "link-${local.vnet_name}"
      virtual_network_id = module.virtual_network.resource_id
      autoregistration   = false
    }
  }

  tags             = local.common_tags
  enable_telemetry = false

  depends_on = [module.virtual_network]
}

# Storage account β€” Standard LRS, StorageV2, TLS 1.2, no public access (FR-020–FR-023).
# Access via private endpoint only; shared-key (SAS) access disabled.
# NOTE: shared_access_key_enabled = false requires Kerberos/AADKERB for SMB
# authentication from the VM.  See quickstart.md Step 9 for mapping instructions.
module "storage_account" {
  source  = "Azure/avm-res-storage-storageaccount/azurerm"
  version = "0.6.7"

  name                = local.storage_account_name
  location            = module.resource_group.location
  resource_group_name = module.resource_group.name

  account_kind             = "StorageV2"
  account_tier             = "Standard" # Standard tier β€” FR-020
  account_replication_type = "LRS"      # Locally-redundant storage β€” FR-020
  min_tls_version          = "TLS1_2"   # Minimum TLS 1.2 enforced β€” FR-020

  # Disable all public network access β€” FR-021; access via private endpoint only
  public_network_access_enabled = false

  # Network rules β€” deny all public traffic; allow Azure services for diagnostics
  network_rules = {
    bypass         = ["AzureServices"]
    default_action = "Deny"
  }

  # Shared key (SAS) access is disabled.  All data-plane operations (including
  # file share creation) use Azure AD auth via provider storage_use_azuread = true
  # (declared in terraform.tf).  See FR-020 and quickstart.md Step 9.
  shared_access_key_enabled = false

  # Azure Files share β€” 100 GB quota (FR-022)
  shares = {
    workload_share = {
      name  = var.storage_file_share_name
      quota = var.storage_file_share_quota_gb
    }
  }

  # Private endpoint for the "file" sub-resource only β€” FR-023
  private_endpoints = {
    file_pe = {
      name                          = local.pe_storage_name
      subnet_resource_id            = module.virtual_network.subnets["pe_subnet"].resource_id
      subresource_name              = "file"
      private_dns_zone_resource_ids = toset([module.private_dns_zone.resource_id])
    }
  }

  # Storage account–level diagnostics (metrics only β€” storage accounts
  # do not support log categories at the account level)
  diagnostic_settings_storage_account = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
      metric_categories     = ["AllMetrics"]
    }
  }

  # Azure Files service–level diagnostics (logs + metrics)
  diagnostic_settings_file = {
    to_law = {
      name                  = "diag-to-law"
      workspace_resource_id = module.log_analytics_workspace.resource_id
    }
  }

  tags             = local.common_tags
  enable_telemetry = false

  depends_on = [module.private_dns_zone]
}

# ─── Observability (US4) ─────────────────────────────────────────────────────

# All diagnostic settings are declared inline with each module call above.
# This section contains only the three native azurerm alert rule resources
# for which no AVM module exists (Constitution Principle II).

# ─── Alerts ──────────────────────────────────────────────────────────────────

# Alert 1: VM stopped / deallocated (FR-027)
# VmAvailabilityMetric = 1 when running, 0 when stopped.  A platform metric β€”
# no Azure Monitor Agent required.  Fires within alert_vm_metric_window_size
# of the VM transitioning to stopped/deallocated.
resource "azurerm_monitor_metric_alert" "vm_stopped" {
  name                = local.alert_vm_stopped_name
  resource_group_name = module.resource_group.name
  scopes              = [module.virtual_machine.resource_id]
  description         = "Alert fires when the VM is in a stopped/deallocated state."
  severity            = 1 # Critical
  enabled             = true

  frequency   = "PT1M"                          # Evaluation frequency: every 1 minute
  window_size = var.alert_vm_metric_window_size # Configurable window (default PT5M)

  criteria {
    metric_namespace = "Microsoft.Compute/virtualMachines"
    metric_name      = "VmAvailabilityMetric"
    aggregation      = "Average"
    operator         = "LessThan"
    threshold        = 1
  }

  # No action group β€” portal-only alerts (clarification Q3)
  tags = local.common_tags
}

# Alert 2: Disk free space < threshold (FR-028)
# PREREQUISITE: Azure Monitor Agent (AMA) + Data Collection Rule (DCR) with
# "LogicalDisk % Free Space" counter must be deployed on the VM before this
# alert produces results (FR-030 exception β€” AMA is a manual post-deploy step,
# see quickstart.md Step 10).
resource "azurerm_monitor_scheduled_query_rules_alert_v2" "disk_low" {
  name                = local.alert_disk_full_name
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
  scopes              = [module.log_analytics_workspace.resource_id]
  description         = "Alert fires when VM disk free space drops below ${var.alert_disk_free_threshold_pct}%."
  severity            = 2 # Warning
  enabled             = true

  evaluation_frequency = var.alert_disk_query_window
  window_duration      = var.alert_disk_query_window

  criteria {
    query = <<-QUERY
      Perf
      | where ObjectName == "LogicalDisk"
          and CounterName == "% Free Space"
          and InstanceName != "_Total"
          and InstanceName != "HarddiskVolume3"
      | where CounterValue < ${var.alert_disk_free_threshold_pct}
      | project TimeGenerated, Computer, InstanceName, CounterValue
    QUERY

    time_aggregation_method = "Count"
    threshold               = 0
    operator                = "GreaterThan"

    failing_periods {
      minimum_failing_periods_to_trigger_alert = 1
      number_of_evaluation_periods             = 1
    }
  }

  # No action group β€” portal-only
  tags = local.common_tags
}

# Alert 3: Key Vault access failures (FR-029)
# Fires on any non-200 KV API response (auth failures, authorization denials,
# throttling).  Requires KV audit diagnostic logs enabled (done inline above).
resource "azurerm_monitor_metric_alert" "kv_access_failures" {
  name                = local.alert_kv_failures_name
  resource_group_name = module.resource_group.name
  scopes              = [module.key_vault.resource_id]
  description         = "Alert fires when Key Vault API requests result in failure responses."
  severity            = 2 # Warning
  enabled             = true

  frequency   = "PT5M"                          # Evaluation frequency: every 5 minutes
  window_size = var.alert_kv_metric_window_size # Configurable window (default PT15M)

  criteria {
    metric_namespace = "Microsoft.KeyVault/vaults"
    metric_name      = "ServiceApiResult"
    aggregation      = "Count"
    operator         = "GreaterThan"
    threshold        = 0

    dimension {
      name     = "StatusCode"
      operator = "Exclude"
      values   = ["200"]
    }
  }

  # No action group β€” portal-only
  tags = local.common_tags
}
# =============================================================================
# variables.tf β€” Input variable declarations for My Legacy Workload
#
# Workload : My Legacy Workload (001-my-legacy-workload)
# Variables are grouped by concern to match the section structure in
# terraform.tfvars.  All defaults reflect the single production environment
# targeted by this configuration (westus3 / prod / legacy).
# =============================================================================

# ─── Global ──────────────────────────────────────────────────────────────────

variable "location" {
  type        = string
  default     = "westus3"
  description = "Azure region for all resources in this workload."
}

variable "environment" {
  type        = string
  default     = "prod"
  description = "Deployment environment label used in resource names and tags (e.g. prod, dev, staging)."
}

variable "workload" {
  type        = string
  default     = "legacy"
  description = "Short workload identifier used in resource names and tags."
}

variable "tags" {
  type        = map(string)
  default     = { environment = "prod", workload = "legacy" }
  description = "Base tag map merged with workload/environment/managedBy/region tags for every resource."
}

# ─── Networking ──────────────────────────────────────────────────────────────

variable "vnet_address_space" {
  type        = list(string)
  default     = ["10.0.0.0/16"]
  description = "CIDR address space assigned to the virtual network."
}

variable "subnet_bastion_cidr" {
  type        = string
  default     = "10.0.0.0/26"
  description = "Address prefix for the AzureBastionSubnet.  Must be /26 or larger to satisfy Azure Bastion requirements."
}

variable "subnet_vm_cidr" {
  type        = string
  default     = "10.0.1.0/24"
  description = "Address prefix for the VM subnet.  VMs are deployed here with NAT outbound only β€” no public IPs."
}

variable "subnet_pe_cidr" {
  type        = string
  default     = "10.0.2.0/24"
  description = "Address prefix for the private-endpoint subnet.  Private endpoints for storage are placed here."
}

# ─── Virtual Machine ─────────────────────────────────────────────────────────

variable "vm_sku_size" {
  type        = string
  default     = "Standard_D2s_v3"
  description = "Azure VM SKU β€” must provide >= 2 vCPU and >= 8 GB RAM (FR-008)."
}

variable "vm_admin_username" {
  type        = string
  default     = "vmadmin"
  description = "Local administrator username for the Windows VM."

  validation {
    condition     = length(var.vm_admin_username) > 0
    error_message = "vm_admin_username must not be empty."
  }
}

variable "vm_image_publisher" {
  type        = string
  default     = "MicrosoftWindowsServer"
  description = "Publisher of the VM source image."
}

variable "vm_image_offer" {
  type        = string
  default     = "WindowsServer"
  description = "Offer of the VM source image."
}

variable "vm_image_sku" {
  type        = string
  default     = "2016-Datacenter"
  description = "SKU of the VM source image (FR-007: Windows Server 2016)."
}

variable "vm_os_disk_type" {
  type        = string
  default     = "Standard_LRS"
  description = "Storage type for the OS disk (Standard_LRS = Standard HDD, FR-008)."
}

variable "vm_data_disk_size_gb" {
  type        = number
  default     = 500
  description = "Size of the data disk in GB (FR-009: 500 GB)."

  validation {
    condition     = var.vm_data_disk_size_gb >= 1
    error_message = "vm_data_disk_size_gb must be at least 1 GB."
  }
}

variable "vm_data_disk_type" {
  type        = string
  default     = "Standard_LRS"
  description = "Storage type for the data disk (Standard_LRS = Standard HDD, FR-009)."
}

variable "vm_computer_name" {
  type        = string
  default     = "leg-prod-001"
  description = "Windows computer (NetBIOS) name for the VM.  Must be <= 15 characters (FR-013).  The Azure resource name is controlled by local.vm_name."

  # CHK032: computer name must fit in NetBIOS 15-char limit and follow DNS rules
  validation {
    condition     = length(var.vm_computer_name) <= 15 && can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$", var.vm_computer_name))
    error_message = "vm_computer_name must be 15 characters or fewer, contain only alphanumeric characters and hyphens, and must not start or end with a hyphen (FR-013, CHK032)."
  }
}

variable "vm_availability_zone" {
  type        = number
  default     = 1
  description = "Availability zone number (1, 2, or 3) for the VM and NAT gateway public IP (FR-014)."

  # CHK033: zone 0 and -1 are explicitly prohibited
  validation {
    condition     = contains([1, 2, 3], var.vm_availability_zone)
    error_message = "vm_availability_zone must be 1, 2, or 3. Values 0 and -1 are explicitly prohibited (FR-015, CHK033)."
  }
}

# ─── Key Vault ───────────────────────────────────────────────────────────────

variable "kv_sku" {
  type        = string
  default     = "standard"
  description = "Key Vault SKU tier (standard or premium)."
}

variable "vm_admin_password_secret_name" {
  type        = string
  default     = "vm-admin-password"
  description = "Name of the Key Vault secret that holds the VM administrator password (FR-018)."
}

# ─── Storage ─────────────────────────────────────────────────────────────────

variable "storage_file_share_name" {
  type        = string
  default     = "share-legacy-prod"
  description = "Name of the Azure Files file share."
}

variable "storage_file_share_quota_gb" {
  type        = number
  default     = 100
  description = "Quota of the file share in GB."
}

# ─── Log Analytics ───────────────────────────────────────────────────────────

variable "log_analytics_retention_days" {
  type        = number
  default     = 30
  description = "Number of days to retain logs in the Log Analytics workspace (minimum 30 for compliance)."
}

# ─── Alert Thresholds ────────────────────────────────────────────────────────

variable "alert_disk_free_threshold_pct" {
  type        = number
  default     = 10
  description = "Disk available percentage below which the disk-full alert fires (FR-028)."
}

variable "alert_vm_metric_window_size" {
  type        = string
  default     = "PT5M"
  description = "ISO 8601 evaluation window for the VM availability metric alert (FR-027)."
}

variable "alert_disk_query_window" {
  type        = string
  default     = "PT15M"
  description = "ISO 8601 evaluation frequency and window for the disk-space scheduled query alert (FR-028)."
}

variable "alert_kv_metric_window_size" {
  type        = string
  default     = "PT15M"
  description = "ISO 8601 evaluation window for the Key Vault access-failure metric alert (FR-029)."
}
# =============================================================================
# outputs.tf β€” Output declarations for My Legacy Workload
#
# Outputs expose the resource IDs and names that downstream consumers
# (pipelines, runbooks, or child modules) need.  Credential values are never
# output β€” the VM password lives only in random_password state and in Key
# Vault; it is never surfaced here.
# =============================================================================

# ─── Resource Group ──────────────────────────────────────────────────────────

output "resource_group_id" {
  description = "Resource ID of the workload resource group."
  value       = module.resource_group.resource_id
}

# ─── Networking ──────────────────────────────────────────────────────────────

output "virtual_network_id" {
  description = "Resource ID of the virtual network."
  value       = module.virtual_network.resource_id
}

output "subnet_vm_id" {
  description = "Resource ID of the VM subnet."
  value       = module.virtual_network.subnets["vm_subnet"].resource_id
}

output "subnet_bastion_id" {
  description = "Resource ID of the Azure Bastion subnet."
  value       = module.virtual_network.subnets["bastion_subnet"].resource_id
}

output "subnet_pe_id" {
  description = "Resource ID of the private-endpoint subnet."
  value       = module.virtual_network.subnets["pe_subnet"].resource_id
}

# ─── Key Vault ───────────────────────────────────────────────────────────────

output "key_vault_id" {
  description = "Resource ID of the Key Vault (not a credential β€” safe to share downstream)."
  value       = module.key_vault.resource_id
  sensitive   = false
}

output "key_vault_name" {
  description = "Name of the Key Vault."
  value       = local.key_vault_name
}

# ─── Storage ─────────────────────────────────────────────────────────────────

output "storage_account_id" {
  description = "Resource ID of the storage account (not a credential β€” safe to share downstream)."
  value       = module.storage_account.resource_id
  sensitive   = false
}

output "storage_account_name" {
  description = "Name of the storage account."
  value       = local.storage_account_name
}

# ─── Virtual Machine ─────────────────────────────────────────────────────────

output "vm_id" {
  description = "Resource ID of the virtual machine."
  value       = module.virtual_machine.resource_id
}

output "vm_name" {
  description = "Azure resource name of the virtual machine."
  value       = local.vm_name
}

# ─── Observability ───────────────────────────────────────────────────────────

output "log_analytics_workspace_id" {
  description = "Resource ID of the Log Analytics workspace."
  value       = module.log_analytics_workspace.resource_id
}

# ─── Bastion ─────────────────────────────────────────────────────────────────

output "bastion_name" {
  description = "Azure resource name of the Bastion host."
  value       = local.bastion_name
}
# =============================================================================
# terraform.tf β€” Provider and Terraform version requirements
#
# Workload : My Legacy Workload (001-my-legacy-workload)
# Region   : West US 3 (westus3)
# This file declares the minimum Terraform version and every provider required
# by this configuration.  AVM modules that use azapi or time internally will
# inherit these constraints automatically.
# =============================================================================

terraform {
  required_version = ">= 1.10, < 2.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.37"
    }
    azapi = {
      source  = "azure/azapi"
      version = "~> 2.4"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
    time = {
      source  = "hashicorp/time"
      version = ">= 0.9.0, < 1.0.0"
    }
  }
}

# -----------------------------------------------------------------------------
# Provider: azurerm
# features {} block is mandatory even when no sub-features are customised.
# The azapi + time + random providers require no additional configuration.
# -----------------------------------------------------------------------------
provider "azurerm" {
  features {}

  # Use Azure AD auth for all storage data-plane operations (queue, blob, file,
  # table probes) instead of shared-key / SAS.  Required because the storage
  # account has allowSharedKeyAccess = false, and the provider's Read() function
  # normally queries queue properties via key auth.
  storage_use_azuread = true
}
  1. Review and approve all changes suggested by Copilot by clicking on the “Keep” button or tweak them as necessary!
  2. It is recommended to make a commit now to capture your implementation results, with a comment of something like Implementation complete.

Next Steps

Congratulations! You’ve walked through a complete Spec Kit workflow for building Azure infrastructure using Azure Verified Modules. By following this structured approach, you’ve created a deployable IaC solution that is:

  • Well-documented: Every design decision is captured in the specification and plan.
  • Secure by default: The constitution enforces security baselines from the start.
  • Reproducible: The generated IaC template can be deployed consistently across environments.
  • Maintainable: Clear task breakdowns and checklists make future updates straightforward.

From here, you can ask Copilot to help you with the deployment and further enhancements, or you can manually take the following steps to deploy and manage your solution:

  1. Validate with What-If: Run az deployment group what-if to preview changes before deployment.
  2. Deploy to Azure: Use the Azure CLI or Bicep CLI to deploy your generated main.bicep to a subscription:
    az deployment group create \
      --resource-group <your-resource-group> \
      --template-file main.bicep \
      --parameters main.bicepparam
  3. Integrate into CI/CD: Add the generated templates to your Azure DevOps or GitHub Actions pipelines.
  4. Extend the solution: Iterate on the specification to add new capabilities while maintaining alignment with your constitution.
  1. Initialize: Run terraform init to download the required providers and modules.
  2. Validate with Plan: Run terraform plan to preview changes before deployment.
  3. Deploy to Azure: Use the Terraform CLI to deploy your generated configuration to a subscription:
    terraform apply
  4. Integrate into CI/CD: Add the generated templates to your Azure DevOps or GitHub Actions pipelines.
  5. Extend the solution: Iterate on the specification to add new capabilities while maintaining alignment with your constitution.

For more information on Spec Kit and the underlying methodology, see the Spec Kit overview or explore the Specification-Driven Development concepts.

Specification-Driven Development (SDD)

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

Overview

Specification-Driven Development (SDD) is a development paradigm where the specification becomes the single source of truth, and code is generated, validated, and continuously regenerated from that specification. The key idea: you define intent upfront and unambiguously, and both humans and AI agents produce the implementation from it.

In this new model, specifications serve as a machine-enforceable contract between:

  • Solution builders who compose IaC solution templates for their workload’s requirements
  • AI assistants that generate code following these requirements
  • Governance teams who can trust that deployed infrastructure meets organizational requirements

This contract ensures that as requirements change, Azure evolves or best practices advance, updates to specifications automatically propagate through AI-assisted development, keeping all solutions aligned with current standards without requiring manual intervention across thousands of code repositories.

Core Principles

  1. The Specification becomes the system: SDD flips the traditional hierarchy: instead of writing code and using specs as optional documentation, code now serves the specification, not the other way around. Specs no longer describe the system - they define it.
  2. Architecture becomes executable: Architecture and requirements aren’t advisory; platforms can enforce them, regenerate code, and detect drift via continuous validation and schema checks.
  3. Intent > Implementation: Human authority shifts “upward,” focusing on intent, policy, constraints, and ethics, while automation handles consistent implementation.
  4. Parallelization and consistency: Because every team consumes the same precise blueprint, SDD eliminates ambiguity and reduces rework.
  5. AI-native development workflow: AI coding agents (e.g., GitHub Copilot with Spec Kit) rely on specifications to generate architecture plans, tests, tasks, and code in a deterministic, repeatable way.

Paradigm Shift

Historically, infrastructure development has been an iterative process of trial and error - developers write code, test it, encounter issues, consult documentation, refine the approach, and repeat. This cycle is time-consuming and error-prone, with each developer potentially interpreting best practices differently, leading to inconsistent implementations across teams and projects.

Specification-driven development represents a fundamental shift in how we approach infrastructure coding. Rather than developers manually translating requirements into code while attempting to remember and apply countless best practices, this approach leverages comprehensive, machine-readable specifications that define exactly how infrastructure should be structured, configured, and implemented.

This paradigm shift elevates the developer’s role from code writer to solution architect. Instead of spending time ensuring compliance with specifications manually, developers can:

  • Design at a higher level: Focus on business requirements and architectural decisions
  • Compose solutions faster: Leverage pre-validated patterns and modules
  • Maintain quality effortlessly: Specifications are automatically applied through AI assistance
  • Scale best practices: Consistent, high-quality implementations across the entire organization

New Development Workflow

Specification-driven development enabled by AI transforms the traditional workflow into a systematic, compliance-first process:

flowchart LR
    A[**Express intent**:<br/>Describe what you want to achieve in natural language] --> B[**AI interprets specifications**:<br/>Copilot consults specifications to understand the compliant implementation path]
    B --> C[**Generate compliant code**:<br/>Produce IaC that adheres to all relevant standards and patterns]
    C --> D[**Validate automatically**:<br/>Built-in awareness of specifications enables immediate validation against requirements]
    D --> E[**Iterate with confidence**:<br/>Modifications and enhancements maintain compliance throughout the development lifecycle]

How Infrastructure-as-Code (IaC) Changes with SDD

When GitHub Copilot is equipped with specifications, AI doesn’t just suggest code - it becomes a compliance engine that understands and enforces the intricate rules, patterns, and best practices defined in the specifications. This carries several advantages.

1. IaC moves from code-first to specification-first

Today’s IaC flow often tries to encode architecture through Bicep/Terraform solution templates. In SDD:

  • The infrastructure specification sits above the IaC language.
  • IaC (Bicep, Terraform solution templates) becomes generated artifacts.
  • Updating the infrastructure means updating the specification, and IaC regeneration ensures consistency. Code is “the last-mile expression” of the spec.

This reduces cognitive load as focus shifts from “How do I implement this correctly?” to “What do I want to accomplish?”

2. Eliminates drift between architecture documents and IaC

  • SDD enforces consistency through continuous schema validation, contract testing, and automated detection of spec-to-code mismatches.
  • This means no more documentation vs solution code mismatches: code is always aligned with the specification.

3. IaC solution templates become generated, not hand-coded

  • Your specification becomes the authoritative source (constraints, principles, etc.).
  • Bicep/Terraform solution templates are generated from specs, by referencing AVM modules - removing human variation.
  • Quality improves as every parameter, and configuration follows the same high-quality standards
  • Refactoring becomes updating specification, not rewriting code
  • Template structure, testing, and documentation become deterministic output.
  • Developers gain access to expert-level knowledge embedded in the specifications without needing to memorize hundreds of pages of requirements

4. Stronger governance built-in from day 1

  • Specs can encode: Well-Architected principles, compliance constraints, naming, tagging, and security baselines
  • IaC code is generated to comply automatically as SDD encodes governance in specifications, providing governance-first enforcement from the beginning.

5. AI agents can automate infra decisions reliably

“Ad-hoc” AI-generated IaC often lacks correctness or compliance; with SDD:

  • Correctness by design is embedded by design, as AI agents use the specifications as guardrails.
  • Generated templates follow a deterministic architecture plan, not LLM “best guesses.”
  • Changes are applied by updating the specification, not patching IaC manually.

This is transformative for large-scale infrastructure modernization.

6. Cross-organizational alignment becomes much easier
IaC solution developers work with various teams, often in different organizations - everyone reads specifications differently. SDD solves this as specifications are versioned, reviewable, and auditable, with decisions and trade-offs stored in specifications. This means fewer misinterpretations, such as requirement mismatches or lifecycle ambiguities.

7. Infrastructure testing and validation become automated

Specifications become the basis for testing, including deployment validations, compliance checks, etc. IaC test automation becomes spec-driven and auto-generated.

The Future is Specification-Driven

As AI capabilities continue to advance, the value of comprehensive, well-defined specifications only increases. The combination of AVM’s rigorous specification framework, the principles of spec-driven development, and GitHub Copilot’s AI intelligence represents not just an incremental improvement, but a fundamental re-imagining of how cloud infrastructure development can and should work in the AI era.

AI-Assisted Module Development

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

For Module Owners

This section covers module development and maintenance scenarios and is primarily relevant for AVM module owners. If you are an end user looking to leverage AVM to develop complex solutions using AI, see AI-Assisted IaC Solution Development.

Overview

AVM module owners and members of the AVM core team can leverage GitHub Copilot and related AI capabilities to accelerate the development, maintenance, and validation of AVM Bicep modules. By embedding AVM-specific knowledge directly into the development environment, we reduce manual effort, improve consistency, and help ensure compliance with AVM specifications from the start.

The AVM core team is building a set of AI-powered assets that GitHub Copilot can use to provide AVM-aware assistance. These assets fall into several categories:

  • Custom Instructions: Markdown files that automatically guide GitHub Copilot’s behavior whenever it generates or reviews code, providing persistent context about the project’s baseline, such as coding standards and compliance requirements.
  • Custom Agents: Specialized AI personas that focus on a specific workflow (e.g., planning, implementing, or validating module changes), each with a defined scope, tools, and the ability to hand off work to other agents.
  • Prompt Files: Reusable, parameterized task templates that define a specific analysis or action for Copilot to perform, targeting specific modules or files.
  • Skills: Detailed, step-by-step workflow definitions that encode domain-specific expertise. Skill descriptions are included in the system prompt on every chat turn, but their full content is loaded into context on demand β€” only when the model determines relevance to the user’s request or the user explicitly invokes the skill.

The actual implementations of these assets may vary between Bicep and Terraform, and they will evolve over time as best practices emerge and capabilities mature. The AVM core team will continue adding documentation and examples as we make progress - both developing new assets from scratch and refining existing ones.

Coming Soon

Content for AI-assisted AVM Terraform module development is currently being developed and will be available soon. Stay tuned for updates!

Subsections of AI-Assisted Module Dev

AVM Bicep AI Assets

Experimental Content

The content in this section represents experimental exploration of emerging technologies and innovative approaches. To learn more about our experimental content and its implications, please refer to the Experimental Section Overview.

This page provides a catalog of all AI-assisted development assets available in the Bicep Registry Modules (BRM) repository. Each asset is linked to its source file for full details.

Custom Instructions

Custom instructions provide persistent, repository-wide context that shapes how GitHub Copilot generates and reviews code. They are automatically loaded by Copilot when working in the repository.

Global Instructions

Global instructions provide GitHub Copilot with an overview of the AVM Bicep repository structure, critical compliance requirements (including mandatory AVM specification adherence), tool usage guidance, module discovery methods (AVM module index, MCR, Azure Resource Reference), and schema/API version lookup procedures. This is the primary instruction file that ensures all AI-generated code aligns with AVM standards.

See the full content of the global instructions here: .github/copilot-instructions.md

Skills

Skills encode complex, multi-step domain workflows into structured instructions that GitHub Copilot can follow precisely. They define prerequisites, rules, phases, and expected outputs. When the IDE starts, skill descriptions are indexed so that Copilot is aware of their existence and purpose. The full content of a skill is only loaded into the context when the user’s prompt references a topic that aligns with the skill’s described purpose.

Child Module Publishing

Source: .github/skills/avm-child-module-publishing/SKILL.md
Description: Guides the end-to-end workflow for publishing a Bicep child module to the AVM public registry. Covers prerequisite verification, allowed-list registration, telemetry instrumentation, version file creation, changelog updates, parent module updates, and final validation. This skill implements the official Child Module Publishing workflow.

Example prompt:

Publish AVM child module avm/res/network/virtual-network/virtual-network-peering

Note: in some cases, VS Code is not identifying the correct skill based on the above prompt. If this happens, you can try to use the follow prompt that explicitly triggers the correct skill:

/avm-child-module-publishing Publish AVM child module avm/res/network/virtual-network/virtual-network-peering

Prompt Files

Prompt files are reusable, parameterized task templates that can be invoked to perform specific analysis or planning tasks against a target module.

Name / SourceDescriptionExample
Tech-debt-analysis .github/prompts/avm.tech-debt-analysis-ARM-API.prompt.mdAnalyzes AVM Bicep modules for technical debt, including inconsistencies, documentation gaps, spelling mistakes, conflicting information, and quality issues across module files and examples. This is a read-only analysis β€” it does not modify any files./AVM-Tech-debt-analysis
AVM-Update-module-with-latest-ARM-API-versions .github/prompts/avm.create-plan-update-ARM-API.prompt.mdAnalyzes AVM Bicep modules to identify resources with outdated ARM API versions, compares them against the latest stable versions, and generates detailed implementation plans for required updates. This is a planning-only task β€” it does not modify any files./AVM-Update-module-to-latest-API-versions

AVM Terraform AI Assets

Coming Soon

Content for AI-assisted AVM Terraform module development is currently being developed and will be available soon. Stay tuned for updates!

Subsections of Help & Support

Subsections of Issue Triage

AVM Issue Triage

“AVM Core Team Triage” Explained

This page provides guidance for members of the AVM Core Team on how to triage module proposals and generic issues filed in the AVM repository, as well as how to manage these GitHub issues throughout their lifecycle.

During the AVM Core Team Triage step, the following will be checked, completed and actioned by the AVM Core Team during their triage calls (which are currently twice per week).

Note

Every module needs a module proposal to be created in the AVM repository.

Tip

During the triage process, the AVM Core Team should also check the status of following queries:

Module Proposal triage

An issue is considered to be a module proposal if

  • it was opened through the “New AVM Module Proposal πŸ“” template,
  • it has the labels of Β Needs: Triage πŸ”Β  and Β Type: New Module Proposal πŸ’‘Β  applied to it, and
  • it is assigned to the “AVM - Module Triage” GitHub project.

Follow these steps to triage a module proposal:

  1. Add the Β Status: In Triage πŸ”Β  label to indicate you’re in the process of triaging the issue.

  2. Check module proposal issue/form:

    • Check the Bicep or Terraform module indexes for the proposed module to make sure it is not already available or being worked on.
    • Ensure the module’s details are correct as per specifications - naming, classification (resource/pattern) etc.
    • Check if the module is added to the “Proposed” column on the AVM - Modules Triage GitHub project board.
    • Check if the requestor is a Microsoft FTE.
    • If there’s any additional clarification needed, contact the requestor through comments (using their GH handle) or internal channels - for Microsoft FTEs only! You can look them up by their name or using the Microsoft Open Source Management Portal’s People finder: “Linked people across Microsoft organizations”. Make sure you capture any decisions regarding the module in the comments section.
    • Make adjustments to the module’s name/classification as needed.
    • Change the name of the issue to reflect the module’s name, i.e.,
      • After the “[Module Proposal]:” prefix, change the issues name to the module’s approved name between backticks, i.e., ` and `, e.g., avm/res/sql/managed-instance for a Bicep module, or avm-res-compute-virtualmachine for a Terraform module.
      • Example:
        • “[Module Proposal]: avm/res/sql/managed-instance
        • “[Module Proposal]: avm-res-sql-managedinstance
    • Check if the GitHub Policy Service Bot has correctly applied the module language label: Β Language: Bicep πŸ’ͺΒ  or Β Language: Terraform 🌐 
  3. Apply relevant labels

    • Module classification (resource/pattern/utility): Β Class: Resource Module πŸ“¦Β , Β Class: Pattern Module πŸ“¦Β  or Β Class: Utility Module πŸ“¦Β 

Triaging pattern modules

As part of the triage of pattern modules, the following points need to be considered/clarified with the module requestor:

  • Shouldn’t this be a resource module? What makes it a pattern - e.g., does it deploy multiple resources?
  • What is it for? What problem does it fix or provides a solution for?
  • What is/isn’t part of it? Which resource and/or pattern modules are planned to be leveraged in it? Provide a list of resources that would be part of the planned module.
  • Where is it coming from/what’s backing it - e.g., Azure Architecture Center (AAC), community request, customer example. Provide an architectural diagram and related documentation if possible - or a pointer to these if they are publicly available.
  • Don’t let the module’s scope to grow too big, split it up to multiple smaller ones that are more maintainable - e.g., hub & spoke networking should should be split to a generic hub networking and multiple workload specific spoke networking patterns.
  • The module’s name should be as descriptive as possible.
  • Adopt strict name-to-scope mapping - e.g., hub & spoke networking shouldn’t deploy monitoring.

Scenario 1: Requestor doesn’t want to / can’t be module owner

Note

If requestor is interested in becoming a module owner, but is not a Microsoft FTE, the AVM core team will try to find a Microsoft FTE to be the module owner whom the requestor can collaborate with.

  1. If the requestor indicated they didn’t want to or can’t become a module owner (or is not a Microsoft FTE), make sure the Β Needs: Module Owner πŸ“£Β  label is assigned to the issue. Note: the GitHub Policy Service Bot should automatically do this, based on how the issue author responded to the related question.
  2. Move the issue to the “Looking for owners” column on the AVM - Modules Triage GitHub project board.
  3. Add a comment on the issue with the #RFRC tag to indicate that the repository should be created. This allows the module to be added the module indexes in the Proposed state, so that it can be found by the community and potential module owners.
  4. Find module owners - if the requestor didn’t volunteer in the module proposal OR the requestor does not want or cannot be owner of the module:
    • Try to find an owner from the AVM communities or await a module owner to comment and propose themselves on the proposal issue.
  5. When a new potential owner is identified, continue with the steps described as follows.

Scenario 2: Requestor wants to and can become module owner

If the requestor indicated they want to become the module owner, the GitHub Policy Service Bot will add the  Status: Owners Identified 🀘  label and will assign the issue to the requestor.

You MUST still confirm that the requestor is a Microsoft FTE and that they understand the implications of becoming the owner! If any of these conditions aren’t met, remove the Β Status: Owners Identified 🀘  label and unassign the issue from the requestor.

  1. Make sure the requestor is a Microsoft FTE. You can look them up by their name or using the Microsoft Open Source Management Portal’s People finder: “Linked people across Microsoft organizations”.
  2. Clarify the roles and responsibilities of the module owner:
    • Clarify they understand and accept what “module ownership” means by replying in a comment to the requestor/proposed owner:
βž• Standard AVM Core Team Reply to Proposed Module Owners
<!-- markdownlint-disable -->
Hi @avm_module_owner,

Thanks for requesting/proposing to be an AVM module owner!

We just want to confirm **you agree to the below pages** that define what module ownership means:

- [Team Definitions & RACI](https://azure.github.io/Azure-Verified-Modules/specs/shared/team-definitions)
- [Module Specifications](https://azure.github.io/Azure-Verified-Modules/specs/module-specs)
- [Module Support](https://azure.github.io/Azure-Verified-Modules/help-support/module-support)

Any questions or clarifications needed, let us know!

If you agree, please just **reply to this issue with the exact sentence below** (as this helps with our automation πŸ‘):

"I CONFIRM I WISH TO OWN THIS AVM MODULE AND UNDERSTAND THE REQUIREMENTS AND DEFINITION OF A MODULE OWNER"

Thanks,

The AVM Core Team

#RR
<!-- markdownlint-restore -->
  1. Once module owner identified has confirmed they understand and accept their roles and responsibilities as an AVM module owner
    • Make sure the issue is assigned to the confirmed module owner.
    • Move the issue into the “In development” column on the AVM - Modules Triage GitHub Project board.
    • Add a comment on the issue with the #RFRC tag to indicate that the repository should be created. This allows the module to be added the module indexes in the Proposed state, so that it can be found by the community.
    • Make sure the Β Status: Owners Identified 🀘  label is added to the issue.
      • If applied earlier, remove the Β Needs: Module Owner πŸ“£Β  label from the issue.
    • Remove the labels of Β Needs: Triage πŸ”Β  and Β Status: In Triage πŸ”Β  to indicate you’re done with triaging the issue.
  2. Update the AVM Module Indexes, following the process documented internally.
  3. Use the following text to approve module development
βž• Final Confirmation for Proposed Module Owners - Bicep
<!-- markdownlint-disable -->
Hi @avm_module_owner,

Thanks for confirming that you wish to own this AVM module and understand the related requirements and responsibilities!

Before starting development, please ensure ALL the following requirements are met.

**Please use the following values explicitly as provided in the [module index](https://azure.github.io/Azure-Verified-Modules/indexes/) page**:

- For your module:
  - `ModuleName` - for naming your module
  - `TelemetryIdPrefix` - for your module's [telemetry](https://azure.github.io/Azure-Verified-Modules/spec/SFR3)
  - Folder path are defined in `RepoURL`.
  - Create GitHub teams for module owners and contributors and grant them permissions as outlined [here](https://azure.github.io/Azure-Verified-Modules/spec/SNFR20).

Check if this module exists in the other IaC language. If so, collaborate with the other owner for consistency. πŸ‘

You can now start the development of this module! βœ… Happy coding! πŸŽ‰

**Please respond to this comment and request a review from the AVM core team once your module is ready to be published! Please include a link pointing to your PR, once available. πŸ™**

Any further questions or clarifications needed, let us know!

Thanks,

The AVM Core Team
<!-- markdownlint-restore -->
βž• Final Confirmation for Proposed Module Owners - Terraform
<!-- markdownlint-disable -->
Hi @avm_module_owner,

Thanks for confirming that you wish to own this AVM module and understand the related requirements and responsibilities!

Check if this module exists in the other IaC language. If so, collaborate with the other owner for consistency. πŸ‘

You can now start the development of this module! βœ… Happy coding! πŸŽ‰ If you have additional contributors, ensure you grant them permissions by adding them to the related GitHub teams, as outlined [here](https://azure.github.io/Azure-Verified-Modules/spec/SNFR20).

**Please respond to this comment and request a review from the AVM core team once your module is ready to be published! Please include a link pointing to your PR, once available. πŸ™**

Any further questions or clarifications needed, let us know!

Thanks,

The AVM Core Team
<!-- markdownlint-restore -->
Important

Although, it’s not directly part of the module proposal triage process, to begin development, module owners and contributors might need additional help from the AVM core team, such as:

  1. Update any Azure RBAC permissions for test tenants/subscription, if needed.
  2. In case of Bicep modules only:
    • Look for the module owners confirmation on the related [Module Proposal] issue that they have created the required -module-owners- GitHub team.
    • Ensure the -module-owners- GitHub team has been assigned to its respective parent team as outlined here.
    • Ensure the CODEOWNERS file in the BRM repo has been updated.
    • Ensure the AVM Module Issue template file in the BRM repo has been updated.

Post-Development issue management

Once module is developed and v0.1.0 has been published to the relevant registry

  1. Assign the  Status: Module Available 🟒  label to the issue.
  2. Move the issue into “Done” column in AVM - Modules Triage GitHub Project.
  3. Update the AVM Module Indexes, following the process documented internally.
  4. When all development actions are complete and confirmed
    1. In case of Bicep modules - Close the orphaned module issue with the following message:

      βž• Closing remarks for the New Owner(s) of an Orphaned Module
      - [x] New owner has been added to the related GitHub team as maintainer.
      - [x] `ORPHANED` file deleted, `README` file updated.
      
      The module index will be updated soon to reflect this change.
      
      Thank you for your work @replace_with_author! I'm closing this issue now.
    2. In case of Terraform modules - Close the issue.

Important
  • The Module Proposal issue MUST remain open until the module is fully developed, tested and published to the relevant registry.
  • Do NOT close the issue before the successful publication is confirmed!
  • Once the module is fully developed, tested and published to the relevant registry, and the Module Proposal issue was closed, it MUST remain closed.

Changing module owners

There can be several reasons why a module owner change is needed, e.g., the current owner is leaving the company, changing team, or is no longer able to maintain the module. In such cases, the module ownership needs to be transferred to a new owner. While in most cases the module needs to be marked as orphaned until it’s taken over by a new module owner, sometimes, the ownership can be transferred through a “hot swap”, where the current owner directly hands over ownership to another person without the module becoming orphaned first.

Note

The original Module Proposal issue related to the module in question MUST remain closed and intact.

Instead, a new Orphaned Module issue must be opened that MUST remain open until the ownership is fully confirmed!

Once the Orphaned Module issue was closed, it MUST remain closed. If the module will subsequently become orphaned again, a new Orphaned Module issue must be opened.

Orphaned modules

If a module meets the criteria described in the “Orphaned Modules” chapter, the module is considered to be orphaned and the below steps must be performed.

When a module becomes orphaned

  1. Submit an “orphaned module” issue by using the “Orphaned AVM Module 🟑” issue template.
  2. Make sure the Β Needs: Triage πŸ”Β , Β Needs: Module Owner πŸ“£Β , and the Β Status: Module Orphaned 🟑  labels are assigned to the issue and it is assigned to the “AVM - Module Triage” GitHub project.
  3. Move the issue into the “Orphaned” column on the AVM - Modules Triage GitHub Project board.
  4. Update the AVM Module Indexes, following the process documented internally.
  5. Place an information notice as per the below guidelines:
    • In case of a Bicep module:
      • Place the information notice - with the text below - in an ORPHANED.md file, in the module’s root.
      • Run the utilities/tools/Set-AVMModule.ps1 utility with the module path as an input. This re-generates the module’s README.md file, so that the README.md file will also contain the same notice in its header.
      • Make sure the content of the ORPHANED.md file is displayed in the README.md in its header (right after the title).
    • In case of a Terraform module, place the information notice - with the text below - in the README.md file, in the module’s root.
    • Once the information notice is placed, submit a Pull Request.

Include the following text in the information notice:

βž• Orphaned module notice for module README file
⚠️THIS MODULE IS CURRENTLY ORPHANED.⚠️

- Only security and bug fixes are being handled by the AVM core team at present.
- If interested in becoming the module owner of this orphaned module (must be Microsoft FTE), please look for the related "orphaned module" GitHub issue [here](https://aka.ms/AVM/OrphanedModules)!
  1. Try to find a new owner using the AVM communities or await a new module owner to comment and propose themselves on the issue.

When a new owner is identified

Tip

To look for Orphaned Modules:

  1. When a new potential owner is identified, clarify the roles and responsibilities of the module owner:
    • Clarify they understand and accept what “module ownership” means by replying in a comment to the requestor/proposed owner:
βž• Standard AVM Core Team Reply to New Owners of an Orphaned Module
<!-- markdownlint-disable -->
Hi @avm_module_owner,

Thanks for requesting/proposing to be an AVM module owner!

We just want to confirm **you agree to the below pages** that define what module ownership means:

- [Team Definitions & RACI](https://azure.github.io/Azure-Verified-Modules/specs/shared/team-definitions)
- [Module Specifications](https://azure.github.io/Azure-Verified-Modules/specs/module-specs)
- [Module Support](https://azure.github.io/Azure-Verified-Modules/help-support/module-support)

Any questions or clarifications needed, let us know!

If you agree, please just **reply to this issue with the exact sentence below** (as this helps with our automation πŸ‘):

"I CONFIRM I WISH TO OWN THIS AVM MODULE AND UNDERSTAND THE REQUIREMENTS AND DEFINITION OF A MODULE OWNER"

Thanks,

The AVM Core Team

#RR
<!-- markdownlint-restore -->
  1. Once the new module owner candidate has confirmed they understand and accept their roles and responsibilities as an AVM module owner
    • Assign the issue to the confirmed module owner.
    • Remove the Β Status: Module Orphaned 🟑  and the Β Needs: Module Owner πŸ“£Β  labels from the issue.
    • Add the Β Status: Module Available 🟒  and Β Status: Owners Identified 🀘  labels to the issue.
    • Move the issue into the “Done” column on the AVM - Modules Triage GitHub Project board.
  2. Update the AVM Module Indexes, following the process documented internally.
  3. Get the new owner(s) added to the related -module-owners- team as applicable. See SNFR20 for more details.
  4. Remove the information notice (i.e., the file that states that ⚠️THIS MODULE IS CURRENTLY ORPHANED.⚠️, etc. ):
    • In case of a Bicep module:
      • Delete the ORPHANED.md file from the module’s root.
      • Run the utilities/tools/Set-AVMModule.ps1 utility with the module path as an input. This re-generates the module’s README.md file, so that it will no longer contain the orphaned module notice in its header.
      • Double check the previous steps was successful and the README.md file no longer has the information notice in its header (right after the title).
    • In case of a Terraform module, remove the information notice from the README.md file in the module’s root.
    • Once the information notice is removed, submit a Pull Request.
  5. Use the following text to confirm the new ownership of an orphaned module:
βž• Final Confirmation for New Owners of an Orphaned Module
Hi @avm_module_owner,

Thanks for confirming that you wish to own this AVM module and understand the related requirements and responsibilities!

We just want to ask you to double check a few important things.

**Please use the following values explicitly as provided in the [module index](https://azure.github.io/Azure-Verified-Modules/indexes/) page**:

- You must become the owner (maintainer) of the GitHub team as outlined [here](https://azure.github.io/Azure-Verified-Modules/spec/SNFR20). If the previous owner is no longer available, submit an ticket via [aka.ms/opensource/ticket](https://aka.ms/opensource/ticket) to take ownership of the team.
- If applicable, remove the "Orphaned module" information notice from the module's `README.md` file as per [these instructions](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/avm-issue-triage/#when-a-new-owner-is-identified) page.
- Please check back in a bit to make sure that your name has been updated in the module index page (this should happen shortly after you confirmed ownership).

You're now the owner of this module and can start improving it as needed! βœ… Happy coding! πŸŽ‰

Any further questions or clarifications needed, let us know!

Thanks,

The AVM Core Team
  1. When all actions detailed above are complete and confirmed, close the orphaned module issue with the following message:
βž• Closing remarks for the New Owner(s) of an Orphaned Module
- [x] New owner has been added to the related GitHub team as maintainer.
- [x] `ORPHANED` file deleted, `README` file updated.

The module index will be updated soon to reflect this change.

Thank you for your work @replace_with_author! I'm closing this issue now.

Hot swapping module owners

When the module owner needs to be changed without the module becoming orphaned, the overall process described in the Orphaned modules chapter needs to be followed, with a few differences.

  1. Submit an “orphaned module” issue by using the “Orphaned AVM Module 🟑” issue template while indicating the GitHub handle of the new owner.
  2. Clarify the roles and responsibilities of the module owner by replying in a comment to the requestor/proposed owner:
βž• Standard AVM Core Team Reply to the New Owner(s) of an Orphaned Module
<!-- markdownlint-disable -->
Hi @avm_module_owner,

Thanks for requesting/proposing to be an AVM module owner!

We just want to confirm **you agree to the below pages** that define what module ownership means:

- [Team Definitions & RACI](https://azure.github.io/Azure-Verified-Modules/specs/shared/team-definitions)
- [Module Specifications](https://azure.github.io/Azure-Verified-Modules/specs/module-specs)
- [Module Support](https://azure.github.io/Azure-Verified-Modules/help-support/module-support)

Any questions or clarifications needed, let us know!

If you agree, please just **reply to this issue with the exact sentence below** (as this helps with our automation πŸ‘):

"I CONFIRM I WISH TO OWN THIS AVM MODULE AND UNDERSTAND THE REQUIREMENTS AND DEFINITION OF A MODULE OWNER"

Thanks,

The AVM Core Team

#RR
<!-- markdownlint-restore -->
  1. Assign the issue to the new module owner.
  2. Remove these labels:
    • Β Needs: Triage πŸ”Β 
    • Β Needs: Module Owner πŸ“£Β 
    • Β Status: Module Orphaned 🟑 
  3. Add these labels to the issue:
    • Β Status: In Triage πŸ”Β 
    • Β Status: Module Available 🟒 
    • Β Status: Owners Identified 🀘  labels to the issue.
    • Module classification (resource/pattern/utility): Β Class: Resource Module πŸ“¦Β , Β Class: Pattern Module πŸ“¦Β  or Β Class: Utility Module πŸ“¦Β 
  4. Make sure the issue is assigned to the “AVM - Module Triage” GitHub project, but don’t move the issue to the “Orphaned” column of this board as it will be automatically moved to the “Done” column, once the issue is closed.
  5. Once the new owner provided their written consent in a comment by replying the text quoted in the message above, update the AVM Module Indexes, following the process documented internally.
  6. Use the following text to finalize the new ownership transfer:
βž• Final Confirmation for the New Owner(s) of an Orphaned Module
Hi @avm_module_owner,

Thanks for confirming that you wish to own this AVM module and understand the related requirements and responsibilities!

We just want to ask you to double check a few important things.

**Please use the following values explicitly as provided in the [module index](https://azure.github.io/Azure-Verified-Modules/indexes/) page**:

- You must become the owner (maintainer) of the GitHub team as outlined [here](https://azure.github.io/Azure-Verified-Modules/spec/SNFR20). If the previous owner is no longer available, submit an ticket via [aka.ms/opensource/ticket](https://aka.ms/opensource/ticket) to take ownership of the team.
- If applicable, remove the "Orphaned module" information notice from the module's `README.md` file as per [these instructions](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/avm-issue-triage/#when-a-new-owner-is-identified) page.
- Please check back in a bit to make sure that your name has been updated in the module index page (this should happen shortly after you confirmed ownership).

You're now the owner of this module and can start improving it as needed! βœ… Happy coding! πŸŽ‰

Any further questions or clarifications needed, let us know!

Thanks,

The AVM Core Team
  1. When all actions detailed above are complete and confirmed, close the orphaned module issue with the following message:
βž• Closing remarks for the New Owner(s) of an Orphaned Module
- [x] New owner has been added to the related GitHub team as maintainer.
- [x] `ORPHANED` file deleted, `README` file updated.

The module index will be updated soon to reflect this change.

Thank you for your work @replace_with_author! I'm closing this issue now.

Deprecated modules

When a module becomes deprecated

If a module meets the criteria described in the “Deprecated Modules” chapter, the module is considered to be deprecated and the below steps must be performed.

Language agnostic steps

  1. Submit a “deprecated module” issue by using the “Deprecate AVM Module πŸ”΄” issue template.
  2. Make sure the Β Needs: Triage πŸ”Β  and the Β Status: Module Deprecated πŸ”΄Β  labels are assigned to the issue and it is assigned to the “AVM - Module Triage” GitHub project.
  3. Update the AVM Module Indexes, following the process documented internally.

Bicep specific steps

  1. Update the module and connected files as per the below guidelines:
    1. Place the information notice - with the text below - in an DEPRECATED.md file, in the module’s root.
    2. Run the utilities/tools/Set-AVMModule.ps1 utility with the module path & -SkipBuild switch as an input. This re-generates the module’s README.md file, so that the README.md file will also contain the same notice in its header. For more instructions on how to use the script, please refer to the corresponding section in the Contribution Guide.
    3. Make sure the content of the DEPRECATED.md file is displayed in the README.md in its header (right after the title).
    4. Add the the notice NOTE: This is the last published version and the module has since been deprecated. to the top-most ### Changes section of the module’s CHANGELOG.md file
    5. Remove the module workflow from the workflows folder.
    6. Make sure the module is removed from the avm_module_issue.yml issue template.
    7. Remove the module from the CODEOWNERS file.
    8. Submit a Pull Request
    9. For the AVM maintainers: Once the PR is merged, run the .Platform - Publish [moduleIndex.json] workflow with the regenIndexFromBRM flag set. This will de-list the module so that it won’t show up in the VS-Code Bicep extension going forward.
  2. Delete the module’s -owners- GitHub teams.

Terraform specific steps

  1. Place the information notice - with the text below - in the README.md file, in the module’s root.
  2. Archive the module’s repository on GitHub.

Deprecation information notice (to be place in the module’s repository as described above)

βž• Deprecated module indicators
⚠️THIS MODULE IS DEPRECATED.⚠️

- It will no longer receive any updates.
- If the underlying Azure service is not deprecated/retired, this module may still be used as is (references to any existing versions will keep working), but it is not recommended for new deployments.
- It is recommended to migrate to a replacement/alternative version of the module, if available.

General feedback/question, documentation update and other standard issues

An issue is a “General Question/Feedback ❔” if it was opened through the “General Question/Feedback ❔” issue template, and has the labels of Β Type: Question/Feedback πŸ™‹β€β™€οΈΒ  and Β Needs: Triage πŸ”Β  applied to it.

An issue is a “AVM Documentation Update πŸ“˜” if it was opened through the “AVM Documentation Update πŸ“˜” issue template, and has the labels of Β Type: Documentation πŸ“„Β  and Β Needs: Triage πŸ”Β  applied to it.

An issue is considered to be a “standard issue” or “blank issue” if it was opened without using an issue template, and hence it does NOT have any labels assigned, OR only has the Β Needs: Triage πŸ”Β  label assigned.

When triaging the issue, consider adding one of the following labels as fits:

  • Β Type: Documentation πŸ“„Β 
  • Β Type: Feature Request βž•Β 
  • Β Type: Bug πŸ›Β 
  • Β Type: Security Bug πŸ”’Β 

To see the full list of available labels, please refer to the GitHub Repo Labels section.

Note

If an intended module proposal was mistakenly opened as a “General Question/Feedback ❔” or other standard issue, and hence, it doesn’t have the Β Type: New Module Proposal πŸ’‘Β  label associated to it, a new issue MUST be created using the “New AVM Module Proposal πŸ“” issue template. The mistakenly created “General Question/Feedback ❔” or other standard issue MUST be closed.

AVM Organizer Bot

Overview

The AVM Organizer Bot is represented as a GitHub App currently named AVM Team Linter (which will be renamed to AVM Organizer in the future). This bot automates various repository management tasks across the Azure Verified Modules program’s repositories, including issue triage, pull request labeling, team validation, and documentation updates.

The bot operates by authenticating with GitHub using the GitHub App credentials (TEAM_LINTER_APP_ID and TEAM_LINTER_PRIVATE_KEY) and executing PowerShell scripts through scheduled workflows and/or event-triggered actions.


AVM Repository Scripts

The following scripts are leveraged by the AVM Organizer Bot in the AVM repository:

1. Invoke-AvmGitHubTeamLinter.ps1

Purpose: Validates GitHub team configurations against module ownership data from CSV indexes.

Description: This script compares the module indexes with existing GitHub Teams configuration to ensure proper team setup. It can validate Bicep parent team configurations, Terraform team permissions, and generate GitHub issues for any discrepancies found. The script supports filtering by module type (Resource/Pattern/Utility) and language (Bicep/Terraform), and can validate -owner- teams.

Key Functionality:

  • Compares module ownership data from CSV files with GitHub team configurations
  • Validates parent team configuration for Bicep module owner teams
  • Verifies correct repository permissions for Terraform teams
  • Creates GitHub issues for unmatched or misconfigured teams
  • Closes resolved GitHub issues when team configurations are corrected

Workflow: github-teams-check-existence.yml (runs Monday-Friday at 10:00 AM and on-demand)

Source Code: Invoke-AvmGitHubTeamLinter.ps1


2. New-AzAdvertizerDiffIssue.ps1

Purpose: Monitors AzAdvertizer data changes and creates tracking issues.

Description: This script creates GitHub issues when data in AzAdvertizer (including PSRule, APRL, and Advisor) changes compared to the last workflow run. It downloads artifacts from previous workflow runs, compares the data to identify changes, and automatically creates issues with detailed diff information when new policy rules, advisories, or recommendations are detected.

Key Functionality:

  • Downloads CSV artifacts from the latest workflow run
  • Compares current AzAdvertizer data with previous data to detect changes
  • Formats detected changes into readable GitHub issue format
  • Creates GitHub issues for new PSRule, APRL, or Azure Advisor data
  • Exports current data as artifacts for future comparisons

Workflow: platform.new-AzAdvertizer-diff-issue.yml (runs weekly on Sundays at 3:00 AM and on-demand)

Source Code: New-AzAdvertizerDiffIssue.ps1


BRM Repository Scripts

The following scripts are leveraged by the AVM Organizer Bot in the bicep-registry-modules (BRM) repository:

1. Set-AvmGitHubIssueOwnerConfig.ps1

Purpose: Automatically assigns issues to appropriate module owners.

Description: This script processes GitHub issues in the BRM repository and automatically assigns them to the correct module owners based on the AVM CSV data. It notifies module owners via comments, assigns the issue to them, adds appropriate labels, and assigns issues to the AVM project board. The script handles both individual issue processing (when triggered by issue creation) and batch processing (when run on schedule).

Key Functionality:

  • Matches issues to modules based on issue content and labels
  • Retrieves module ownership information from AVM CSV indexes (Resource, Pattern, Utility)
  • Automatically assigns issues to designated module owners
  • Posts notification comments mentioning module owners
  • Adds appropriate labels based on module type and status
  • Assigns issues to GitHub project boards for tracking
  • Handles orphaned modules by assigning to core team
  • Tracks statistics on assignments and updates

Workflow: platform.set-avm-github-issue-owner-config.yml (runs on issue creation, weekly on Sundays at midnight, and on-demand)

Source Code: Set-AvmGitHubIssueOwnerConfig.ps1


2. Set-AvmGitHubPrLabels.ps1

Purpose: Automatically labels pull requests based on reviewer requirements.

Description: This script evaluates newly created or ready-for-review pull requests and adds appropriate labels to indicate whether the PR can be approved by module owners or requires core team review. It analyzes the requested reviewer teams and module ownership data to determine if a module is orphaned or if the sole module owner is the PR author, both scenarios requiring core team intervention.

Key Functionality:

  • Retrieves PR information including author and requested reviewer teams
  • Identifies module-specific reviewer teams (excluding core teams)
  • Checks if core team is already assigned as reviewer
  • Validates module ownership and team membership
  • Adds Β Needs: Core Team 🧞  label when module is orphaned or has insufficient owners
  • Adds Β Needs: Module Owner πŸ“£Β  label when module owners can review
  • Adds Β Status: Module Orphaned 🟑  label for orphaned modules
  • Automatically adds module team members as reviewers when appropriate

Workflow: platform.set-avm-github-pr-labels.yml (runs when PRs are opened or marked ready for review)

Source Code: Set-AvmGitHubPrLabels.ps1


3. Set-AvmGitHubIssueForWorkflow.ps1

Purpose: Creates and manages issues for failed workflow runs.

Description: This script monitors workflow run status and automatically creates GitHub issues when module or platform workflows fail. When a workflow fails, it creates an issue with links to the failed run and assigns it to the appropriate module owners. If the workflow subsequently succeeds, the script automatically closes the issue and adds a comment with the successful run link. This ensures prompt notification and tracking of CI/CD pipeline failures.

Key Functionality:

  • Monitors all GitHub workflow runs in the repository
  • Filters out ignored workflows (e.g., PSRule checks, PR title checks)
  • Creates new issues for failed workflow runs with detailed information
  • Links issues to the specific failed workflow run
  • Assigns issues to module owners based on workflow name
  • Automatically closes issues when workflows succeed after previous failures
  • Adds comments to existing issues for repeated failures or successes
  • Assigns workflow failure issues to GitHub project boards
  • Tracks and reports statistics on issues created, closed, and updated

Workflow: platform.manage-workflow-issue.yml (runs daily at 5:30 AM and on-demand)

Source Code: Set-AvmGitHubIssueForWorkflow.ps1


4. Sync-AvmModulesList.ps1

Purpose: Compares the module list in issue templates with CSV data. If not in sync, it creates an issue to update the template.

Description: This script ensures that the module list in the GitHub issue template (avm_module_issue.yml) remains synchronized with the AVM CSV data. It compares available and orphaned modules from the CSV indexes (Resource, Pattern, and Utility) against the modules listed in the issue template. When discrepancies are detected (missing modules or unexpected modules), the script creates a GitHub issue detailing the necessary changes to bring the template into alignment with the current module inventory.

Key Functionality:

  • Loads module data from AVM CSV indexes for Resources, Patterns, and Utilities
  • Filters for available and orphaned top-level modules
  • Parses the GitHub issue template to extract currently listed modules
  • Identifies missing modules that should be added to the template
  • Identifies unexpected modules that should be removed from the template
  • Creates detailed GitHub issues with lists of required changes
  • Assigns synchronization issues to the AVM project board
  • Ensures issue template stays current as modules are added or deprecated

Workflow: platform.sync-avm-modules-list.yml (runs daily at 4:30 AM and on-demand)

Source Code: Sync-AvmModulesList.ps1


Summary

The AVM Organizer Bot leverages these automation scripts to maintain repository health, ensure proper module ownership and team configurations, keep documentation current, and provide timely notifications about workflow failures and policy changes. The bot operates continuously through scheduled workflows and event-triggered actions, reducing manual overhead for the AVM core team and module owners while ensuring consistent governance across both repositories.

BRM Issue Triage

Overview

This page provides guidance for Bicep module owners on how to triage AVM module issues and AVM question/feedback items filed in the BRM repository (Bicep Registry Modules repository - where all Bicep AVM modules are published), as well as how to manage these GitHub issues throughout their lifecycle.

As such, the following issues are to be filed in the BRM repository:

  • [AVM Module Issue]: Issues specifically related to an existing AVM module, such as feature requests, bug and security bug reports.
  • [AVM Question/Feedback]:Generic feedback and questions, related to existing AVM module, the overall framework, or its automation (CI environment).

Do NOT file the following types of issues in the BRM repository, as they MUST be tracked in the AVM repo:

Note

Every module needs a module proposal to be created in the AVM repository.

Module Owner Responsibilities

During the triage process, module owners are expected to check, complete and follow up on the items described in the sections below.

Module owners MUST meet the SLAs defined on the Module Support page! While there’s automation in place to support meeting these SLAs, module owners MUST check for new issues on a regular basis.

Important

The BRM repository includes other, non-AVM modules and related GitHub issues. As a module owner, make sure you’re only triaging, managing or otherwise working on issues that are related to AVM modules!

Tip

Module Issue

An issue is considered to be an “AVM module issue” if

  • it was opened through the [AVM Module Issue] template in the BRM repository,
  • it has the labels of Β Needs: Triage πŸ”Β  and Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  applied to it, and
  • it is assigned to the “AVM - Module Issues” GitHub project.
Note

Module issues can only be opened for existing AVM modules. Module issues MUST NOT be used to file a module proposal.

If the issue was opened as a misplaced module proposal, mention the @Azure/AVM-core-team-technical-bicep team in the comment section and ask them to move the issue to the AVM repository.

Triaging a Module Issue

  1. Check the Module issue:
    • Make sure the issue has the Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  applied to it.
    • Use the AVM module indexes to identify the module owner(s) and make sure they are assigned/mentioned/informed.
    • If the module is orphaned (has no owner), make sure there’s an orphaned module issue in the AVM repository.
    • Make sure the module’s details are captured correctly in the description - i.e., name, classification (resource/pattern), language (Bicep/Terraform), etc.
    • Make sure the issue is categorized using one of the following type labels:
      • Β Type: Feature Request βž•Β 
      • Β Type: Bug πŸ›Β 
      • Β Type: Security Bug πŸ”’Β 
  2. Apply relevant labels for module classification (resource/pattern): Β Class: Resource Module πŸ“¦Β  or Β Class: Pattern Module πŸ“¦Β 
  3. Communicate next steps to the requestor (issue author).
  4. Remove the Β Needs: Triage πŸ”Β  label.
  5. When more detailed plans are available, communicate expected timeline for the update/fix to the requestor (issue author).
  6. Only close the issue, once the next version of the module was fully developed, tested and published.

Triaging a Module PR

  1. If the PR is submitted by the module owner and the module is owned by a single person, the AVM core team must review and approve the PR, (as the module owner can’t approve their on PR).
    • To indicate that the PR needs the core team’s attention, apply the Β Needs: Core Team 🧞  label.
  2. If the PR is submitted by a contributor (other than the module owner), or the module is owned by at least 2 people, one of the module owners should review and approve the PR.
  3. Apply relevant labels
    • Categorize the PR using applicable labels, such as:
      • Β Type: Feature Request βž•Β 
      • Β Type: Bug πŸ›Β 
      • Β Type: Security Bug πŸ”’Β 
    • For module classification (resource/pattern): Β Class: Resource Module πŸ“¦Β  or Β Class: Pattern Module πŸ“¦Β 
  4. If the module is orphaned (has no owner), make sure the related Orphaned module issue (in the AVM repository) is associated to the PR in a comment, so the new owner can easily identify all related issues and PRs when taking ownership.
  5. Remove the Β Needs: Triage πŸ”Β  label.
Give your PR a meaningful title
  • Prefix: Start with one of the allowed keywords - fix: or feat: is the most common for module related changes.
  • Description: Add a few words, describing the nature of the change.
  • Module name: Add the module’s full name between backticks ( ` ) to make it pop.
Who needs to approve the PR?

The PR approval logic for existing modules is the following:

PR is submitted by a module ownerPR is submitted by anyone, other than the module owner
Module has a single module ownerAVM core team or in case of Terraform only, the owner of another module approves the PRModule owner approves the PR
Module has multiple module ownersAnother owner of the module (other than the submitter) approves the PROne of the owners of the module approves the PR

This behavior is assisted by bots, through automatic assignment of the expected reviewer(s) and supporting labels.

In case of Bicep modules, if the PR includes any changes outside of the “modules/” folder, it first needs the module related code changes need to be reviewed and approved as per the above table, and only then does the PR need to be approved by a member of the core team. This way the core team’s approval does not act as a bypass from the actual code review perspective.

General Question/Feedback and other standard issues

An issue is considered to be an “AVM Question/Feedback” if

  • it was opened through the [AVM Question/Feedback] template in the BRM repository,
  • it has the labels of Β Needs: Triage πŸ”Β , Β Type: Question/Feedback πŸ™‹β€β™€οΈΒ  and Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  applied to it, and
  • it is assigned to the “AVM - Issue Triage” GitHub project.

An issue is considered to be a “standard issue” or “blank issue” if it was opened without using an issue template, and hence it does NOT have any labels assigned, OR only has the Β Needs: Triage πŸ”Β  label assigned.

Triaging a General Question/Feedback and other standard issues

  1. When triaging the issue, consider adding one of the following labels as fits:

    • Β Type: Documentation πŸ“„Β 
    • Β Type: Feature Request βž•Β 
    • Β Type: Bug πŸ›Β 
    • Β Type: Security Bug πŸ”’Β 

To see the full list of available labels, please refer to the GitHub Repo Labels section.

  1. Add any (additional) labels that apply.
  2. Communicate next steps to the requestor (issue author).
  3. Remove the Β Needs: Triage πŸ”Β  label.
  4. When more detailed plans are available, communicate expected timeline for the update/fix to the requestor (issue author).
  5. Once the question/feedback/topic is fully addressed, close the issue.
Note

If an intended module proposal was mistakenly opened as a “AVM Question/Feedback ❔” or other standard issue, a new issue MUST be created in the AVM repo using the “New AVM Module Proposal πŸ“” issue template. The mistakenly created “AVM Question/Feedback ❔” or other standard issue MUST be closed.

Issue Triage Automation

This page details the automation that is in place to help with the triage of issues and PRs raised against the AVM modules.

Schedule based automation

This section details all automation rules that are based on a schedule.

Note

When calculating the number of business days in the issue/triage automation, the built-in logic considers Monday-Friday as business days. The logic doesn’t consider any holidays.

ITA01BCP.1 & ITA01BCP.2

If a bug/feature/request/general question that has the labels of Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  and Β Needs: Triage πŸ”Β  is not responded to after 3 business days, then the issue will be marked with the Β Status: Response Overdue 🚩  label and the AVM Core team will be mentioned in a comment on the issue to reach out to the module owner.

Schedule:

  • Triggered every Monday-Friday, at 12:00.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 3 business days.
  • Has the Β Needs: Triage πŸ”Β  and Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  labels added.
  • Does not have the Β Status: Response Overdue 🚩  label added.

Action(s):

  • Add a reply, mentioning the Azure/avm-core-team-technical-bicep team.
  • Add the Β Status: Response Overdue 🚩  label.
Tip
  • To prevent further actions to take effect, the Β Status: Response Overdue 🚩  label must be removed, once this issue has been responded to.
  • To avoid this rule being (re)triggered, the Β Needs: Triage πŸ”Β  must be removed as part of the triage process (when the issue is first responded to).

ITA01TF.1 & ITA01TF.2

If a bug/feature/request/general question that has the Β Needs: Triage πŸ”Β  label added is not responded to after 3 business days, then the issue will be marked with the Β Status: Response Overdue 🚩  label and the AVM Core team will be mentioned in a comment on the issue to reach out to the module owner.

Schedule:

  • Triggered every Monday-Friday, at 12:00.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 3 business days.
  • Has the Β Needs: Triage πŸ”Β  label added.

Action(s):

  • Add a reply, mentioning the Azure/avm-core-team-technical-bicep team.
  • Add the Β Status: Response Overdue 🚩  label.
Tip
  • To prevent further actions to take effect, the Β Status: Response Overdue 🚩  label must be removed, once this issue has been responded to.
  • To avoid this rule being (re)triggered, the Β Needs: Triage πŸ”Β  must be removed as part of the triage process (when the issue is first responded to).

ITA02BCP.1 & ITA02BCP.2

If after an additional 3 business days there’s still no update to the issue that has the labels of Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  and Β Status: Response Overdue 🚩 , the AVM core team will be mentioned on the issue and a further comment stating module owner is unresponsive will be added. The Β Needs: Immediate Attention ‼️  label will also be added.

Schedule:

  • Triggered every Monday-Friday, at 12:00.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 3 business days.
  • Has the Β Status: Response Overdue 🚩  and Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  labels added.
  • Does not have the Β Needs: Immediate Attention ‼️  label added.

Action(s):

  • Add a reply, mentioning the Azure/avm-core-team-technical-bicep team.
  • Add the Β Needs: Immediate Attention ‼️  label.
Tip
  • To avoid this rule being (re)triggered, the Β Needs: Triage πŸ”Β  and Β Status: Response Overdue 🚩  labels must be removed when the issue is first responded to!
  • Remove the Β Needs: Immediate Attention ‼️  label once the issue has been responded to.

ITA02TF.1 & ITA02TF.2

If after an additional 3 business days there’s still no update to the issue that has the Β Status: Response Overdue 🚩  label added, the AVM core team will be mentioned on the issue and a further comment stating module owner is unresponsive will be added. The Β Needs: Immediate Attention ‼️  label will also be added.

Schedule:

  • Triggered every Monday-Friday, at 12:00.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 3 business days.
  • Has the Β Status: Response Overdue 🚩  label added.

Action(s):

  • Add a reply, mentioning the Azure/avm-core-team-technical-bicep team.
  • Add the Β Needs: Immediate Attention ‼️  label.
Tip
  • To avoid this rule being (re)triggered, the Β Needs: Triage πŸ”Β  and Β Status: Response Overdue 🚩  labels must be removed when the issue is first responded to!
  • Remove the Β Needs: Immediate Attention ‼️  label once the issue has been responded to.

ITA03BCP

If there’s still no response after 5 days (total from start of issue being raised) on an issue that has the labels of Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ , Β Needs: Triage πŸ”Β , Β Type: Security Bug πŸ”’Β  and Β Status: Response Overdue 🚩 , the Β Needs: Immediate Attention ‼️  label will be added.

Schedule:

  • Triggered every Monday-Friday, at 12:00.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 5 business days.
  • Has the Β Needs: Triage πŸ”Β , the Β Type: Security Bug πŸ”’Β , the Β Status: Response Overdue 🚩 , and Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  labels added.

Action(s):

  • Add the Β Needs: Immediate Attention ‼️  label.
Tip
  • To avoid this rule being (re)triggered, the Β Needs: Triage πŸ”Β  and Β Status: Response Overdue 🚩  labels must be removed when the issue is first responded to!
  • Remove the Β Needs: Immediate Attention ‼️  label once the issue has been responded to.

ITA03TF

If there’s still no response after 5 days (total from start of issue being raised) on an issue that has the labels of Β Needs: Triage πŸ”Β , Β Type: Security Bug πŸ”’Β  and Β Status: Response Overdue 🚩 , the Terraform PG GitHub Team will be mentioned on the issue to assist. The Β Needs: Immediate Attention ‼️  label will also be added.

Schedule:

  • Triggered every Monday-Friday, at 12:00.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 5 business days.
  • Has the Β Needs: Triage πŸ”Β , the Β Type: Security Bug πŸ”’Β , and Β Status: Response Overdue 🚩  labels added.

Action(s):

  • Add a reply, mentioning the Azure/terraform-avm team.
  • Add the Β Needs: Immediate Attention ‼️  label.

ITA04

If an issue/PR has been labelled with Β Needs: Author Feedback πŸ‘‚Β  and hasn’t had a response in 4 days, label with Β Status: No Recent Activity πŸ’€Β  and add a comment.

Schedule:

  • Triggered every 3 hours.

Trigger criteria:

  • Is an open issue/PR.
  • Had no activity in the last 4 days.
  • Has the Β Needs: Author Feedback πŸ‘‚Β  label added.
  • Does not have the Β Status: No Recent Activity πŸ’€Β  label added.

Action(s):

  • Add the Β Status: No Recent Activity πŸ’€Β  label.
  • Add a reply.
Tip

To prevent further actions to take effect, one of the following conditions must be met:

  • The author must respond in a comment within 3 days of the automatic comment left on the issue.
  • The Β Status: No Recent Activity πŸ’€Β  label must be removed.
  • If applicable, the Β Status: Long Term ⏳  or the Β Needs: Module Owner πŸ“£Β  label must be added.

ITA05

Warning

This rule is currently disabled in the AVM and BRM repositories.

If an issue/PR has been labelled with Β Status: No Recent Activity πŸ’€Β  and hasn’t had any update in 3 days from that point, automatically close it and comment, unless the issue/PR has a Β Status: Long Term ⏳  - in which case, do not close it.

Schedule:

  • Triggered every 3 hours.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 3 days.
  • Has the Β Needs: Author Feedback πŸ‘‚Β  and the Β Status: No Recent Activity πŸ’€Β  labels added.
  • Does not have the Β Needs: Module Owner πŸ“£Β  or Β Status: Long Term ⏳  labels added.

Action(s):

  • Add a reply.
  • Close the issue.
Tip
  • In case the issue needs to be reopened (e.g., the author responds after the issue was closed), the Β Status: No Recent Activity πŸ’€Β  label must be removed.

ITA24

Remind module owner(s) to start or continue working on this module if there was no activity on the Module Proposal issue for more than 3 weeks. Add Β Needs: Attention πŸ‘‹Β  label.

Schedule:

  • Triggered every 3 hours.

Trigger criteria:

  • Is an open issue.
  • Had no activity in the last 21 days.
  • Has the Β Type: New Module Proposal πŸ’‘Β  and the Β Status: Owners Identified 🀘  labels assigned.
  • Does not have the Β Status: Long Term ⏳  label assigned.
  • Does not have the Β Needs: Attention πŸ‘‹Β  label assigned.

Action(s):

  • Add a reply.
  • Add the Β Needs: Attention πŸ‘‹Β  label.
Tip
  • To silence this notification, provide an update every 3 weeks on the Module Proposal issue, or add the Β Status: Long Term ⏳  label.

Event based automation

This chapter details all automation rules that are based on an event.

ITA06

When a new issue or PR of any type is created add the Β Needs: Triage πŸ”Β  label.

Trigger criteria:

  • An issue or PR is opened.

Action(s):

  • Add the Β Needs: Triage πŸ”Β  label.
  • Add a reply to explain the action(s).

ITA08BCP

If AVM or “Azure Verified Modules” is mentioned in an uncategorized issue (i.e., one not using any template), apply the label of Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  on the issue.

Trigger criteria:

  • An issue, issue comment, PR, or PR comment is opened, created or edited and the body or comment contains the strings of “AVM” or “Azure Verified Modules”.

Action(s):

  • Add the Β Type: AVM πŸ…°οΈ ✌️ β“œοΈΒ  label.

ITA09

When #RR is used in an issue, add the label of Β Needs: Author Feedback πŸ‘‚Β .

Trigger criteria:

  • An issue comment or PR comment contains the string of “#RR”.

Action(s):

  • Add the Β Needs: Author Feedback πŸ‘‚Β  label.

ITA10

When #wontfix is used in an issue, mark it by using the label of Β Status: Won’t Fix πŸ’”Β  and close the issue.

Trigger criteria:

  • An issue comment or PR comment contains the string of “#RR”.

Action(s):

  • Add the Β Status: Won’t Fix πŸ’”Β  label.
  • Close the issue.

ITA11

When the author replies, remove the Β Needs: Author Feedback πŸ‘‚Β  label and label with Β Needs: Attention πŸ‘‹Β .

Trigger criteria:

  • Any action on an issue comment or PR comment except closing.
  • Has the Β Needs: Author Feedback πŸ‘‚Β  label assigned.
  • The activity was initiated by the issue/PR author.

Action(s):

  • Remove the Β Needs: Author Feedback πŸ‘‚Β  label.
  • Remove the Β Status: No Recent Activity πŸ’€Β  label.
  • Add the Β Needs: Attention πŸ‘‹Β  label.

ITA12

Clean up e-mail replies to GitHub Issues for readability.

Trigger criteria:

  • Any action on an issue comment.

Action(s):

  • Clean email reply. This is useful when someone directly responds to an email notification from GitHub, and the email signature is included in the comment.

ITA13

If the language is set to Bicep in the Module proposal, add the Β Language: Bicep πŸ’ͺΒ  label on the issue.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.
### Bicep or Terraform?

Bicep

Action(s):

  • Add the Β Language: Bicep πŸ’ͺΒ  label.

ITA14

If the language is set to Terraform in the Module proposal, add the  Language: Terraform 🌐  label on the issue.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.
### Bicep or Terraform?

Terraform

Action(s):

  • Add the Β Language: Terraform 🌐  label.

ITA15

Remove the Β Needs: Triage πŸ”Β  label from a PR, if it already has a “Type: XYZΒ  label added and is assigned to someone at the time of creating it.

Trigger criteria:

  • A PR is opened with any of the following labels added and is assigned to someone:
    • Β Type: Bug πŸ›Β 
    • Β Type: Documentation πŸ“„Β 
    • Β Type: Duplicate 🀲 
    • Β Type: Feature Request βž•Β 
    • Β Type: Hygiene 🧹 
    • Β Type: New Module Proposal πŸ’‘Β 
    • Β Type: Question/Feedback πŸ™‹β€β™€οΈΒ 
    • Β Type: Security Bug πŸ”’Β 

Action(s):

  • Remove the Β Needs: Triage πŸ”Β  label.

ITA16

Add the  Status: Owners Identified 🀘  label when someone is assigned to a Module Proposal.

Trigger criteria:

  • Any action on an issue except closing.
  • Has the Β Type: New Module Proposal πŸ’‘Β  added.
  • The issue is assigned to someone.

Action(s):

  • Add the Β Status: Owners Identified 🀘  label.

ITA17

If the issue author says they want to be the module owner, assign the issue to the author and respond to them.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.

    ### Do you want to be the owner of this module?
    
    Yes

Action(s):

  • Assign the issue to the author.

  • Add the below reply and explain the action(s).

    @${issueAuthor}, thanks for volunteering to be a module owner!
    
    **Please don't start the development just yet!**
    
    The AVM core team will review this module proposal and respond to you first. Thank you!

ITA18

Send automatic response to the issue author if they don’t want to be module owner and don’t have any candidate in mind. Add the Β Needs: Module Owner πŸ“£Β  label.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.

      ### Do you want to be the owner of this module?
    
      No
    
      ### Module Owner's GitHub Username (handle)
    
      _No response_

Action(s):

  • Add the Β Needs: Module Owner πŸ“£Β  label.

  • Add the below reply and explain the action(s).

    @${issueAuthor}, thanks for submitting this module proposal!
    The AVM core team will review it and will try to find a module owner.

ITA19

Send automatic response to the issue author if they don’t want to be module owner but have a candidate in mind. Add the Β Status: Owners Identified 🀘  label.

Trigger criteria:

  • An issue is opened with its body matching the below pattern…

      ### Do you want to be the owner of this module?
    
      No
  • …AND NOT matching the below pattern.

    ### Module Owner's GitHub Username (handle)
    
    _No response_

Action(s):

  • Add the Β Status: Owners Identified 🀘  label.

  • Add the below reply and explain the action(s).

    @${issueAuthor}, thanks for submitting this module proposal with a module owner in mind!
    
    **Please don't start the development just yet!**
    
    The AVM core team will review this module proposal and respond to you and/or the module owner first. Thank you!

ITA20

If the issue type is feature request, add the Β Type: Feature Request βž•Β  label on the issue.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.

    ### Issue Type?
    
    Feature Request

Action(s):

  • Add the Β Type: Feature Request βž•Β  label.

ITA21

If the issue type is bug, add the Β Type: Bug πŸ›Β  label on the issue.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.

    ### Issue Type?
    
    Bug

Action(s):

  • Add the Β Type: Bug πŸ›Β  label.

ITA22

If the issue type is security bug, add the Β Type: Security Bug πŸ”’Β  label on the issue.

Trigger criteria:

  • An issue is opened with its body matching the below pattern.

    ### Issue Type?
    
    Security Bug

Action(s):

  • Add the Β Type: Security Bug πŸ”’Β  label.

ITA23

Remove the Β Status: In PR πŸ‘‰Β  label from an issue when it’s closed.

Trigger criteria:

  • An issue is opened.

Action(s):

  • Remove the Β Status: In PR πŸ‘‰Β  label.

ITA25

Inform module owners that they need to add the Β Needs: Core Team 🧞  label to their PR if they’re the sole owner of their module.

Trigger criteria:

  • A PR is opened.

Action(s):

  • Inform module owners that they need to add the Β Needs: Core Team 🧞  label to their PR if they’re the sole owner of their module.

ITA26

Add a label for the AVM Core Team to query called Β Status: Ready For Repository Creation πŸ“Β  when a module owner adds a comment to the issue to tell them.

Trigger criteria:

  • A comment is added to an issue that contains the #RFRC tag.

Action(s):

  • Adds the Β Status: Ready For Repository Creation πŸ“Β  label to the Issue.

ITA27

Add a comment to a PR that modifies these files based on the regex pattern, advising to disable GitHub Actions prior to merging:

  • “.github/actions/templates/avm-**”
  • “.github/workflows/avm.template.module.yml”
  • “utilities/pipelines/**”
  • “!utilities/pipelines/platform/**”

Trigger criteria:

  • A comment is added to an PR that modifies these files (above)

Action(s):

  • A comment is added to an PR that modifies these files as per below

    [!WARNING]
    **FAO: AVM Core Team**
    When merging this PR it will trigger **all** AVM modules to be triggered! Please consider disabling the GitHub actions prior to merging and then re-enable once merged.

Where to apply these rules?

The below table details which repositories the above rules are applied to.

Rules applied for Schedule based automation

IDAVM Core repositoryBRM repositoryTF repositories
ITA01BCP1-2βœ”οΈ
ITA01TF1-2βœ”οΈ
ITA02BCP1-2βœ”οΈ
ITA02TF1-2βœ”οΈ
ITA03BCPβœ”οΈ
ITA03TFβœ”οΈ
ITA04βœ”οΈβœ”οΈβœ”οΈ
ITA05[βœ”οΈ][βœ”οΈ]βœ”οΈ
ITA24βœ”οΈ
Warning

The ITA05 rule is currently disabled in the AVM and BRM repositories.

Rules applied for Event based automation

IDAVM Core repositoryBRM repositoryTF repositories
ITA06βœ”οΈβœ”οΈβœ”οΈ
ITA08BCPβœ”οΈ
ITA09βœ”οΈβœ”οΈβœ”οΈ
ITA10βœ”οΈβœ”οΈβœ”οΈ
ITA11βœ”οΈβœ”οΈβœ”οΈ
ITA12βœ”οΈβœ”οΈβœ”οΈ
ITA13βœ”οΈ
ITA14βœ”οΈ
ITA15βœ”οΈβœ”οΈβœ”οΈ
ITA16βœ”οΈ
ITA17βœ”οΈ
ITA18βœ”οΈ
ITA19βœ”οΈ
ITA20βœ”οΈβœ”οΈ
ITA21βœ”οΈβœ”οΈ
ITA22βœ”οΈβœ”οΈ
ITA23βœ”οΈβœ”οΈ
ITA25βœ”οΈ
ITA26βœ”οΈ
ITA27βœ”οΈ

Terraform Issue Triage

Overview

This page provides guidance for Terraform Module owners on how to triage AVM module issues and AVM question/feedback items filed in their Terraform Module Repo(s), as well as how to manage these GitHub issues throughout their lifecycle.

The following issues can be filed in a Terraform repository:

  • AVM Module Issue: Issues specifically related to an existing AVM module, such as feature requests, bug and security bug reports.
  • AVM Question/Feedback: Generic feedback and questions, related to existing AVM module, the overall framework, or its automation (CI environment).

Do NOT file the following types of issues in a Terraform repository, as they MUST be tracked in the AVM repo:

Note

Every module needs a module proposal to be created in the AVM repository.

Module Owner Responsibilities

During the triage process, module owners are expected to check, complete and follow up on the items described in the sections below.

Module owners MUST meet the SLAs defined on the Module Support page! While there’s automation in place to support meeting these SLAs, module owners MUST check for new issues on a regular basis.

Tip
  • To look for items that need triaging, look for issue labled with ➑️ Β Needs: Triage πŸ”Β β¬…οΈ.
  • To look for items that need attention, look for issue labled with ➑️ Β Needs: Attention πŸ‘‹Β  ⬅️.

Module Issue

An issue is considered to be an “AVM module issue” if

  • it was opened through the AVM Module Issue template in the Terraform repository,
  • it has the label of Β Needs: Triage πŸ”Β  applied to it, and
  • it is assigned to the “AVM - Module Issues” GitHub project.
Note

Module issues can only be opened for existing AVM modules. Module issues MUST NOT be used to file a module proposal.

If the issue was opened as a misplaced module proposal, mention the @Azure/AVM-core-team-technical-terraform team in the comment section and ask them to move the issue to the AVM repository.

Triaging a Module Issue

  1. Check the Module issue:
    • Use the AVM module indexes to identify the module owner(s) and make sure they are assigned/mentioned/informed.
    • If the module is orphaned (has no owner), make sure there’s an orphaned module issue in the AVM repository.
    • Make sure the module’s details are captured correctly in the description - i.e., name, classification (resource/pattern), language (Bicep/Terraform), etc.
    • Make sure the issue is categorized using one of the following type labels:
      • Β Type: Feature Request βž•Β 
      • Β Type: Bug πŸ›Β 
      • Β Type: Security Bug πŸ”’Β 
  2. Apply relevant labels for module classification (resource/pattern): Β Class: Resource Module πŸ“¦Β  or Β Class: Pattern Module πŸ“¦Β 
  3. Communicate next steps to the requestor (issue author).
  4. Remove the Β Needs: Triage πŸ”Β  label.
  5. When more detailed plans are available, communicate expected timeline for the update/fix to the requestor (issue author).
  6. Only close the issue, once the next version of the module was fully developed, tested and published.

Triaging a Module PR

  1. If the PR is submitted by the module owner and the module is owned by a single person, the AVM core team or the owner of another Terraform module must review and approve the PR, (as the module owner can’t approve their on PR).
    • To indicate that the PR needs the core team’s attention, apply the Β Needs: Core Team 🧞  label.
  2. If the PR is submitted by a contributor (other than the module owner), or the module is owned by at least 2 people, one of the module owners should review and approve the PR.
  3. Apply relevant labels
    • Categorize the PR using applicable labels, such as:
      • Β Type: Feature Request βž•Β 
      • Β Type: Bug πŸ›Β 
      • Β Type: Security Bug πŸ”’Β 
    • For module classification (resource/pattern): Β Class: Resource Module πŸ“¦Β  or Β Class: Pattern Module πŸ“¦Β 
  4. If the module is orphaned (has no owner), make sure the related Orphaned module issue (in the AVM repository) is associated to the PR in a comment, so the new owner can easily identify all related issues and PRs when taking ownership.
  5. Remove the Β Needs: Triage πŸ”Β  label.
Give your PR a meaningful title
  • Prefix: Start with one of the allowed keywords - fix: or feat: is the most common for module related changes.
  • Description: Add a few words, describing the nature of the change.

General Question/Feedback and other standard issues

An issue is considered to be an “AVM Question/Feedback” if

  • it was opened through the AVM Question/Feedback template in your Terraform repository,
  • it has the labels of Β Needs: Triage πŸ”Β  and Β Type: Question/Feedback πŸ™‹β€β™€οΈΒ  applied to it, and
  • it is assigned to the “AVM - Issue Triage” GitHub project.

Triaging a General Question/Feedback and other standard issues

  1. When triaging the issue, consider adding one of the following labels as fits:

    • Β Type: Documentation πŸ“„Β 
    • Β Type: Feature Request βž•Β 
    • Β Type: Bug πŸ›Β 
    • Β Type: Security Bug πŸ”’Β 

To see the full list of available labels, please refer to the GitHub Repo Labels section.

  1. Add any (additional) labels that apply.
  2. Communicate next steps to the requestor (issue author).
  3. Remove the Β Needs: Triage πŸ”Β  label.
  4. When more detailed plans are available, communicate expected timeline for the update/fix to the requestor (issue author).
  5. Once the question/feedback/topic is fully addressed, close the issue.
Note

If an intended module proposal was mistakenly opened as a “AVM Question/Feedback ❔” or other standard issue, a new issue MUST be created in the AVM repo using the “New AVM Module Proposal πŸ“” issue template. The mistakenly created “AVM Question/Feedback ❔” or other standard issue MUST be closed.

Known Issues

Unfortunately, there will be times where issues are out of the AVM core team and module owners/contributor’s control and the issue may be something that has to be lived with for a longer than ideal duration - for example, in case of changes that are due to the way the Azure platform, or a resource behaves, or because of an IaC language issue.

This page will detail any of the known issues that consumers may come across when using AVM modules and provide links to learn more about them and where to get involved in discussions on these known issues with the rest of the community.

Important

Issues related to an AVM module must be raised on the repo they are hosted on, not the AVM Central (Azure/Azure-Verified-Modules) repo!

Although, if you think a known issue is missing from this page please create an issue on the AVM Central Azure/Azure-Verified-Modules repo.

If you accidentally raise an issue in the wrong place, we will transfer it to its correct home. πŸ‘

Bicep

Bicep what-if compatibility with modules

Bicep/ARM What-If has a known issue today where it short-circuits whenever a runtime function is used in a nested template. And due to the way Bicep modules work, all module declarations in a Bicep file end up as a resulting nested template deployment in the underlying generated ARM template, thereby invoking this known issue.

GitHub Issue for Further Information & Discussion

The ARM/Bicep Product Group has recently announced on the issue that they are making progress in this space and are aiming provide a closer ETA in the near future; see the comment here.

While this isn’t an AVM issue, we understand that consumers of AVM Bicep modules may want to use what-if and are running into this known issue. Please keep adding your support to the issue mentioned above (Azure/arm-template-whatif #157), as the Product Group are actively engaging in the discussion there. πŸ‘

Other Related GitHub Issues

4MB limitation

A well-known limitation of ARM, and in extension Bicep, is its compiled ARM template size constraint of 4MB. While there is not anything one can do to change this limit there are actions you can take to reduce your template’s size and make it less likely to run into this issue.

In the following we provide you with a list of options you should be aware of:

βž• Use loops for multi-instance deployments

If you deploy multiple instances of the same module (e.g., DNS entries, role assignments, etc.) you should invoke the module using a loop, as opposed to separate references to the same module. The reason comes down the way that ARM interprets these references: Each reference of a module is restored to its full ARM size. That means, if you invoke the same module 3 separate time, you will find that this module’s full template is added as a nested deployment 3 separate times. Using a loop instead, the reference is only added once and invoked as many times as your loop has entries.

For example, you should refactor the code

targetScope = 'subscription'

@description('The principal to assign the roles to.')
param principalId string

module testDeployment1 'br/public:avm/res/authorization/role-assignment/sub-scope:0.1.0' = {
  params: {
    principalId: principalId
    roleDefinitionIdOrName: 'Contributor'
  }
}
module testDeployment2 'br/public:avm/res/authorization/role-assignment/sub-scope:0.1.0' = {
  params: {
    principalId: principalId
    roleDefinitionIdOrName: 'Role Based Access Control Administrator'
  }
}

to

targetScope = 'subscription'

@description('The principal to assign the roles to.')
param principalId string

var rolesToAssign = [
  'Contributor'
  'Role Based Access Control Administrator'
]

module testDeployment 'br/public:avm/res/authorization/role-assignment/sub-scope:0.1.0' = [
  for role in rolesToAssign: {
    params: {
      principalId: principalId
      roleDefinitionIdOrName: role
    }
  }
]

For this example, the compiled JSON of first version has a size of 18kb, the second 10kb.

βž• Only use AVM if you benefit from its features

Using AVM modules can come with a lot of advantages compared to a native resource deployment. This can be as simple as being a ‘module’ deployment, enabling you to deploy to multiple scopes in the same template at once, all the way to encapsulating entire solution into a single invocation and hence drastically reducing the complexity of your own solution template.

However, they also come with certain limitations. For one, that you’re dependent on the module providing you all the features you need, but moreover, that the very same features are always part of the module, whether you use them or not, hence contributing to your solution template’s size.

With this in mind, our recommendation is to only use AVM modules if you use any of its features, hence justifying the added size.

Recommendations

  • Only use the br/public:avm/res/resources/resource-group resource if you deploy resource groups with role assignments
  • Only use the br/public/avm/res|ptn/authorization/(...) modules if you benefit from their scope flexibility
  • When facing challenges with the template size, start replacing individual module references with their native counter-part under consideration of the size-reduction (considering large modules like API-Management, Storage Account, etc.) and the complexity of re-implementing the required features yourself. The good news: For the latter you can cherry-pick the parts of the AVM template you need.
βž• Split the solution template

Probably the most uncomfortable option. If you cannot deploy your solution in one go, it may make sense to split it into logical chunks that you can deploy separately and optionally in sequence (e.g., in your workflow).

This approach comes with a few drawbacks such as the potentially longer deployment time and less intuitive resolution of interdependencies. In other words, if many of your module deployments use each other’s outputs and you split them into multiple templates, you’d need to create ’existing’ references in the later deployments to get the same outputs.

For example, splitting the following two resources

module acr 'br/public:avm/res/container-registry/registry:0.9.3' = {
  params: {
    name: 'myContainerRegistry'
  }
}

module key 'br/public:avm/res/key-vault/vault/key:0.1.0'= {
  params: {
    name: 'myKey'
    keyVaultName: 'keyVaultName'
    roleAssignments: [
      {
        principalId: acr.outputs.systemAssignedMIPrincipalId!
        roleDefinitionIdOrName: 'Key Vault Crypto Service Encryption User'
      }
    ]
  }
}

in two templates requires you to either add an output for the first resource’s identity to the first template and pass it to the second, or create an existing reference in the second template akin to

resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = {
  name: 'myContainerRegistry'
}

module key 'br/public:avm/res/key-vault/vault/key:0.1.0'= {
  params: {
    name: 'myKey'
    keyVaultName: 'keyVaultName'
    roleAssignments: [
      {
        principalId: acr.identity.principalId
        roleDefinitionIdOrName: 'Key Vault Crypto Service Encryption User'
      }
    ]
  }
}

Terraform

Currently there are no known issues for AVM Terraform modules. πŸ₯³

Module Support

Recent Changes to Support Statements

The AVM support statements and targets have been updated as of June 2025. To understand the changes, please review the below updates on this page.

For more information and reasoning behind the changes, please refer to the blog post we published on this topic: Tech Community: Azure Verified Modules: Support Statement & Target Response Times Update

As mentioned on the Introduction page, we understand that long-term support from Microsoft in an initiative like AVM is critical to its adoption by consumers and therefore the success of AVM. Therefore we have aligned and provide the below support statement/process for AVM modules:

Support Statements

Info

Module owners do go on holiday or have periods of leave from time to time, during these times the AVM core team will attempt to triage issues based on the below on behalf of module owners. πŸ‘

For bugs/security issues

  • 5Β businessΒ days for a triage, meaningful response, and ETA to be provided for fix/resolution by module owner (which could be past the 5 days)
    • ForΒ issues that breach the 5 business days, the AVM core team will be notified and will attempt to respond to the issue within an additional 5 business days to assist in triage.
    • For security issues, the Bicep or Terraform Product Groups may step inΒ to resolve security issues, if unresolved, after a further additional 5 business days.

For feature requests

  • 15 business days for a meaningful response and initial triage to understand the feature request. An ETA may be provided by the module owner if possible.
AVM is Open-Source

AVM is open-source, therefore, contributions are welcome via Pull Requests or comments in Issues from anyone in the world at any time on any Pull Request or Issues to assist AVM module owners 🌐

Review the contribution guidance to get involved!

Info

All of this will be automated via the use of the Resource Management feature of the Microsoft GitHub Policy Service and GitHub Actions, where possible and appropriate.

Note

Please note that the durations stated above are for a reasonable and useful response towards resolution of the issue raised, if possible, and not for a fix within these durations; although if possible this will of course happen.

Tip

Issues that are likely related to an AVM module should be directly submitted on the module’s GitHub repository as an “AVM - Module Issue”. To identify the correct code repository, see the AVM module indexes.

If an issue is likely related to the Azure platform, its APIs or configuration, script or programming languages, etc., you need to raise a ticket with Microsoft CSS (Microsoft Customer Services & Support) where your ticket will be triaged for any platform issues. If deemed a platform issue, the ticket will be addressed accordingly. In case it’s deemed not a platform but a module issue, you will be redirected to submit a module issue on GitHub.


Orphaned Modules

Modules that have to have the AVM core team or Product Groups step in due to the module owners/contributors not responding, the AVM module will become “orphaned”; see Module Lifecycle for more info.

Info

If a module is orphaned, the AVM team will try to find a new owner by:

  1. Listing orphaned modules in a saved GitHub issue query on the AVM module index pages for potential new owners to pick up.
  2. In more urgent or high priority cases, selectively identifying a new module owner from the pool of existing AVM module owners/contributors to take over the module.

To raise attention to an orphaned module and allow the AVM team to better prioritize actions, customers can leave a comment on the “orphaned module” issue, explaining their use case and why they would like to see the module supported. This will help the AVM team to prioritize the module for a new owner.

Telemetry

Microsoft uses the approach detailed in this section to identify the deployments of the AVM Modules. Microsoft collects this information to provide the best experiences with their products and to operate their business. Telemetry data is captured through the built-in mechanisms of the Azure platform; therefore, it never leaves the platform, providing only Microsoft with access. Deployments are identified through a specific GUID (Globally Unique ID), indicating that the code originated from AVM. The data is collected and governed by Microsoft’s privacy policies, located at the Trust Center.

Telemetry collected as described here does not provide Microsoft with insights into the resources deployed, their configuration or any customer data stored in or processed by Azure resources deployed by using code from AVM. Microsoft does not track the usage/consumption of individual resources using telemetry described here.

Note

While telemetry gathered as described here is only accessible by Microsoft. Bicep customers have access to the exact same deployment information on the Azure portal, under the Deployments section of the corresponding scope (Resource Group, Subscription, etc.). Terraform customers can view the information sent in the main.telemetry.tf file.

See View deployment history with Azure Resource Manager for further information on how.

Technical Details

As detailed in SFR3 each AVM module contains a avmTelemetry deployment, which creates a deployment such as 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3 (for Bicep) or 46d3xgtf.res.compute-virtualmachine.1-2-3.eum3 (for Terraform).

Opting Out

Albeit telemetry described in this section is optional, the implementation follows an opt-out logic, as most commercial software solutions, this project also requires continuously demonstrating evidence of usage, hence the AVM core team recommends leaving the telemetry setting on its default, enabled configuration.

This resource enables the AVM core team to query the number of deployments of a given module from Azure - and as such, get insights into its adoption.

To opt out you can set the parameters/variables listed below to false in the AVM module:

  • Bicep: enableTelemetry
  • Terraform: enable_telemetry

Telemetry vs Customer Usage Attribution

Though similar in principles, this approach is not to be confused and does not conflict with the usage of CUA IDs that are used to track Azure customer usage attribution of Azure marketplace solutions (partner solutions). The GUID-based telemetry approach described here can coexist and can be used side-by-side with CUA IDs. If you have any partner or customer scenarios that require the addition of CUA IDs, you can customize the AVM modules by adding the required CUA ID deployment while keeping the built-in telemetry solution.

Tip

If you’re a partner and want to build a solution that tracks customer usage attribution (using a CUA ID), we recommend implementing it on the consuming template’s level (i.e., the multi-module solution, such as workload/application) and apply the required naming format 'pid-' (without the suffix).

Resources

This page references additional resources available for Azure Verified Modules (AVM).

Note

Additional internal content available for Microsoft FTEs only, here.

πŸŽ₯ Videos

Community Videos

πŸ“” Blog posts

See the latest blog posts on Azure Verified Modules:

See our former blog posts:

πŸ”¬ Labs

πŸŽ™οΈ Podcasts

πŸ’» Presentations

Subsections of Resources

Community Calls

πŸŽ‰ Join Our Community Calls! πŸŽ‰

πŸ—“οΈ Quarterly calls, rotating time zones for global inclusion. 🌏

🌟 AVM team shares updates and stories.

🎀 Your voice matters: bring questions, stories, and suggestions.

πŸ“½οΈ Missed it? Watch the recording on YouTube!

Let’s make each community call a celebration of connection and collaboration!

Upcoming Events

Stay tuned, to be announced! πŸ‘‚

Previous Events

3rd December, 2025

1st July 2025

6th February 2025


24th September 2024

21st May 2024

Frequently Asked Questions (FAQ)

Tip

Got an unanswered question? Create a GitHub Issue so we can get it answered and added here for everyone’s benefit πŸ‘

Note

Microsoft FTEs only: check out the internal FAQ for additional information.

Tip

Check out the Contribution Q&A for more answers to common questions about the contribution process.

Timeline, history, plans

When will we have a library that has a “usable” stand? Not complete, but the most important resources?

  • Bicep: AVM evolved all modules of CARML (Common Azure Resource Module Library) for its Bicep resource module collection (see here). To initially populate AVM with Bicep resource modules, all existing CARML modules have been migrated to AVM. Resource modules can now be directly leveraged to support the IaC needs of a wide variety of Azure workloads. Pattern modules can also be developed building on these resource modules.
  • Terraform: In case of Terraform, there were significantly less modules available in TFVM (Terraform Verified Modules Library) compared to CARML, hence, most Terraform modules have been and are being built as people volunteer to be module owners. We’ve been prioritizing the development of the Terraform modules based on our learnings from former initiatives, as well as customer demand - i.e., which ones are the most frequently deployed modules.

What happened to existing initiatives like CARML and TFVM?

The AVM team worked/works closely with the teams behind the following initiatives:

Note

AVM is a straight-line evolution of CARML & TFVM.

  • All previously existing assets from these two libraries have been incorporated into AVM as resource or pattern modules.
  • All previously existing (non-AVM) modules that were published in the Public Bicep Registry (stored in the /modules folder of the BRM repository) have either been retired or transformed into an AVM module - while some are still being worked on.

CARML to AVM Evolution

CARML can be considered AVM’s predecessor. It was started by Industry Solutions Delivery (ISD) and the Customer Success Unit (CSU) and has been contributed to by many across Microsoft and has also had external contributions.

A lot of CARML’s principles and architecture decisions have formed the basis for AVM. Following a small number of changes to make them AVM compliant, all CARML modules have been transitioned to AVM as resource or pattern modules.

In summary, CARML evolved to and has been rebranded as the Bicep version of AVM. A notice has been placed on the CARML repo redirecting users and contributors to the AVM central repository.​

Terraform Timeline and Approach

As the AVM core team is not directly responsible for the development of the modules (that’s the responsibility of the module owners), there’s no specific timeline available for the publication of Terraform modules.

However, the AVM core team is focused on the following activities to facilitate and optimize the development process:

  • Leveraging customer demand, telemetry and learnings from former initiatives to prioritize the development of Terraform modules.
  • Providing automated tools and processes (CI environment and automated tests).
  • Accelerating the build-out of the Terraform module owners’ community.
  • Recruiting new volunteers to build and maintain Terraform modules.

Will existing Landing Zone Accelerators (Platform & Application) be migrated to become AVM pattern modules and/or built from AVM resource modules?

Not in the short/immediate term. Existing Landing Zone Accelerators (Platform & Application) will not be forced to convert their existing code bases, if available in either language, to AVM or to use AVM.

However, over time if new features or functionality are required by Landing Zone Accelerators, that team SHOULD consider migrating/refactoring that part of their code base to be constructed with the relevant AVM module if available. For example, the Bicep version of the “Sub Vending” solution is migrating to AVM shortly.

If the relevant AVM module isn’t available to use to assist the Landing Zone Accelerator, then a new AVM module proposal should be made, and if desired, the Landing Zone Accelerator team may decide to own this proposed module πŸ‘


Does/will AVM cover other Microsoft cloud products such as Entra ID, Microsoft 365, GitHub, Azure DevOps, or Microsoft Fabric?

Today, the AVM program is scoped specifically to Azure cloud resources and does not produce standalone modules for other Microsoft cloud products such as Entra ID, Microsoft 365, GitHub, Azure DevOps, or Microsoft Fabric.

AVM’s core value is the standardization of Azure resource configurations via Bicep and Terraform, providing consistent interfaces for things like WAF alignment, RBAC, diagnostic settings, and private networking. We have investigated whether this standardization model can be applied to other Microsoft cloud products (for example, we have spoken to relevant product group teams on Entra ID) and, for today, we have concluded there is no meaningful impact to be had by creating standalone modules for objects such as an Entra ID group.

This is not a permanent decision. Generally speaking, the AVM team is open to any conversation about expanding scope to cover other Microsoft cloud products. If there are valid reasons to wrap these assets inside AVM modules and a recognized standardization surface can be defined (comparable to what AVM provides for Azure resources today), we are open to that conversation. We encourage the community to raise proposals via a GitHub Issue.

What about pattern modules? Assets/objects and their configuration from other Microsoft cloud products may still be included in pattern modules as native asset/object declarations (e.g., via a supported Terraform provider or Bicep extension), where they add meaningful value to the overall architecture pattern being described. Per the AVM specifications (PMNFR2), pattern modules may use native “resource” code where necessary. The restriction above applies specifically to standalone resource modules for these platforms.

Note

If you believe AVM should define a standardization layer for one of these platforms, please open a GitHub Issue with your proposal and rationale.


Will AVM also become a part of azd cli?

Yes, the AVM team is partnering with the AZD team and they are already using Bicep AVM modules from the public registry.


Will AVM support OpenTofu?

Please see our OpenTofu support statement in this discussion on GitHub.


Definitions, comparisons

What is the difference between the Bicep Registry and AVM? (How) Do they come together?

The Public Bicep Registry (backed by the BRM repository) is Microsoft’s official Bicep Registry for 1st party-supported Bicep modules. It has existed for a while now and has seen quite some contributions.

As various teams inside Microsoft have come together to establish a “One Microsoft” IaC approach and library, we started the AVM initiative to bridge the gaps by defining specifications for both Bicep and Terraform modules.

In the BRM repo today, “vanilla modules” (non-AVM modules) can be found in the /modules folder, while AVM modules are located in the /avm folder. Both are being published to the same endpoint, the Public Bicep Registry. AVM Bicep modules are published in a dedicated namespace, using the avm/res & avm/ptn prefixes to make them distinguishable from the Public Registry’s “vanilla modules”.

Note

Going forward, AVM will become the single Microsoft standard for Bicep modules, published to the Public Bicep Registry, via the BRM repository.

In the upcoming period, existing “vanilla” modules will be retired or migrated to AVM, and new modules will be developed according to the AVM specifications.


AVM - with its modules published in the Public Bicep Registry (backed by the BRM repository) - represents the only standard from Microsoft for Bicep modules in the Public Registry.

Bicep private registries and TemplateSpecs are different ways of inner-sourcing, sharing and internally leveraging Bicep modules within an organization. We’re planning to provide guidance for theses scenarios in the future.

AVM has nothing to do with Radius (yet), but the AVM core team is constantly looking for additional synergies inside Microsoft.


What does AVM mean by “WAF Aligned”?

Tip

At a high-level “WAF Aligned” means, where possible and appropriate, AVM Modules will align to recommendations and default input parameters/variables to values that align to high impact/priority/severity recommendations in the following frameworks and resources:

For security recommendations we will also utilize the following frameworks and resources; again only for high impact/priority/severity recommendations:

Will all AVM modules be 100% “WAF Aligned” out of the box and good to go?

Not quite, but they’ll certainly be on the right path. By default, modules will only have to set defaults for input parameters/variables to values that align to high impact/priority recommendations, as detailed above.

To understand this further you first must understand that some of the “WAF Aligned” recommendations, from the sources above are more than just setting a string or boolean value to something particular to meet the recommendation; some will require additional resources to be created and exist and then linked together to help satisfy the recommendation.

In these scenarios the AVM modules will not enforce the additional resources to be deployed and configured, but will provide sufficient flexibility via their input parameters/variables to be able to support the configuration, if so desired by the module consumer.

Tip

This is why we only enforce AVM module alignment to high impact/priority recommendations, as the the majority of recommendations that are not high impact/priority will require additional resources to be used together to be compliant, as the below example will show.

Some examples
RecommendationWill Be Set By Default in AVM Modules?
TLS version should always be set the latest/highest version TLS 1.3Yes, as string value
Key Vault should use RBAC instead of access policies for authorizationYes, as string/boolean value
Container registries should use private linkNo, as requires additional Private Endpoint and DNS configuration as well as, potentially, additional costs
API Management services should use a virtual networkNo, as requires additional Virtual Network and Subnet configuration as well as, potentially, additional costs
Important

While every Well-Architected Framework pillar’s recommendations should equally be considered by the module owners/contributors, within AVM we are taking an approach to prioritize reliability and security over cost optimization. This provides consumers of the AVM modules, by default, more resilient and secure resources and patterns.

However, please note these defaulted values can be altered via input parameter/variables in each of the modules so that you can meet your specific requirements.


What is a “Primary Resource” in the context of AVM?

The definition of a Primary Resource is detailed in the glossary.

How does AVM align and assist with the Secure Future Initiative (SFI)?

AVM modules are continuously being improved with the security and reliability recommendations of the Well-Architected Framework (for more details, see what AVM means by “WAF-aligned”). The AVM team is continuously reviewing SFI recommendations and if required rolling out updates to the AVM initiative to remain SFI compliant as well as assisting module owners to ensure their modules help their consumers align to SFI where appropriate.


Contribution, module ownership

Can I be an AVM module owner if I’m not a Microsoft FTE?

Every module MUST have an owner who is responsible for module development and maintenance. One owner can own one or multiple modules. An owner can develop modules alone or lead a team that will develop a module.

Today, only Microsoft FTEs can be module owners. This is to ensure we can enforce and provide the long-term support required by this initiative.

However, you can still contribute to AVM as a non-Microsoft FTE. For more details, see how you can contribute to AVM without being a module owner below.


How can I contribute to AVM without being a module owner?

Yes, you can contribute to a module without being its owner, but you’ll still need a module owner whom you can collaborate with. For context, see the answer to this question.

Tip

If you’re a Microsoft FTE, you should consider volunteering to be a module owner. You can propose a new module, or look for orphaned modules and volunteer to be the owner for any of them.

If you’re not a Microsoft FTE or don’t want to be a module owner, you can still contribute to AVM. You have multiple options:

  • You can propose a new module and provide as much context as possible under the “Module Details” section (e.g., why do you need the module, what’s the business impact of not having it, etc.). The AVM core team will try to find a Microsoft FTE to be the module owner whom you can collaborate with.
  • You can contact the current owner of any existing module and offer to contribute to it. You can find the current owners of all AVM modules in the module indexes.
  • You can look for orphaned modules and use the comment section to indicate that you’d be interested in contributing to this module, once a new owner is found.

Are there different ways to contribute to AVM?

Yes, there are multiple ways to contribute to AVM!

You can contribute to modules:

  1. Become an owner (preferred):
    • Propose and develop a new module (Bicep or Terraform) or pick up a module someone else proposed.
    • Become the owner of an orphaned module (mainly Bicep) - look for “orphaned module” issues here or see the “Orphaned” swimlane here
  2. Become an administrative owner and work with other contributors or co-owners on developing and maintaining modules.
  3. Volunteer as a co-owner or module contributor to an existing module, and work along other contributors and the (administrative) module owner.
  4. You can submit a PR with a small proposed change without officially becoming a module owner or contributor.

Or you can contribute to the AVM website/documentation, by following this guidance.

Note

New modules can’t be created and published without having a module owner assigned.


Where can I find modules I can contribute to?

You can find modules missing owners in the following places:

  1. All new modules looking for owners or see the “Looking for owners” swimlane here
  2. All Orphaned modules or see the “Orphaned” swimlane here
  3. All modules looking for contributors
Tip

To indicate your interest in owning or contributing to a module, just leave a comment on the respective issue.

Note

If any of these queries don’t return any results, it means that no module in the selected category is looking for an owner or contributor at the moment.


I want to become the owner of XYZ modules, where can I indicate this, and what are the expected actions from me?

If exists, you can comment on the Module Proposal issue of the module that you are interested in and the AVM Core Team will do the triage providing information about next steps.

Having an understanding of roles & responsibilities is useful as well, you can find this information on the Team Definitions & RACI | Azure Verified Modules page.


Can I submit a PR with new features to an existing module? If so, is this a good way to contribute too?

Of course! As all modules are open source, anyone can submit a PR to an existing module. But we’d suggest opening an issue first to discuss the suggested changes with the module owner before investing time in the code.


Are there any videos on how to get started with contribution? E.g., how to set up a local environment for development, how to write a unit test etc.?

No videos on the technical details of contribution are available (yet), but a detailed, written guidance can be found for both Bicep and Terraform, here:


Support

Is AVM a Microsoft official service/product/library or is this classified as an OSS backed by Microsoft?

AVM is an officially supported OSS project from Microsoft, across all organizations.

AVM is owned, developed & supported by Microsoft, you may raise a GitHub issue on this repository or the module’s repository directly to get support or log feature requests.

You can also log a support ticket and these will be redirected to the AVM team and the module owner(s).

See Module Support for more information.


So, does CSS support AVM?

Yes, and if they cannot resolve it (and/or it’s not related to a Microsoft service/platform/api/etc.) they will pass the ticket to the module owner(s) to resolve.

For Microsoft FTEs only: see the Internal wiki for support workflow for more details -AVM - Support Workflow - Overview


How are AVM modules updated/maintained?

Module owners are tasked to do with two types of maintenance:

  • Proactive: keeping track of the modules’ underlying technology evolving, and keep modules up to date with the latest features and API versions.
  • Reactive: sometimes, mistakes are made that result in bugs and/or there might be features consumers ask for faster than module owners could proactively implement them. Consumers can request feature updates and bug fixes for existing modules here.

Can AVM modules be used in production before they are marked as v1.0?

The AVM framework continues to evolve, and several elements, such as Continuous Integration (CI) processes, module specifications and corresponding specification‑validation coverage, are not yet fully implemented. For this reason, modules are currently published as 0.x.y minor versions (e.g., 0.1.0, 0.1.1, 0.2.0, etc.).

However, a module does not need to be at version 1.0.0 to be considered production ready. Consumers can use these modules in any environment, including production. Consumers can also raise issues or feature requests as they learn from the usage of the module, and rely on the detailed changelog files (Bicep) and release notes (Terraform) when assessing whether to upgrade to a newer version. Once a module reaches v1.0, semantic versioning will apply fully: breaking changes will be avoided whenever possible, but if they become necessary, they will be introduced through a major version increment.


Why did the AVM team change the support statements and targets in June 2025?

The AVM team has updated the support statements and targets to better align with the current state of the AVM initiative and to ensure that module owners can provide meaningful responses and resolutions to issues raised by consumers. The changes were made to improve clarity, set realistic expectations, and enhance the overall support experience for AVM modules and their consumers.

More information about the changes can be found in the blog post, Tech Community: Azure Verified Modules: Support Statement & Target Response Times Update, we published on this topic.

Technical questions

Should pattern modules leverage resource modules? What if (some of) the required resource modules are not available?

The initial focus of development and migration from CARML/TFVM has solely been on resource modules. Now that the most important resource modules are published, pattern modules can leverage them as and where needed. This however doesn’t mean that the development of pattern modules is blocked in any way if a resource module is not available, since they may use native resources (“vanilla code”). If you’re about to develop a pattern module and would need a resource modules that doesn’t exist today, please consider building the resource module first, so that others can leverage it for their pattern modules as well.

Please see PMNFR2 for more details.


Does AVM have same limitations as ARM (4 MB) size and 255 parameters only?

Yes, as AVM is just a collection of official Bicep/Terraform modules, it still has same Bicep/Terraform language or Azure platform limitations.


Does/will AVM support Managed Identity, and Microsoft Entra objects automation?

Managed Identities - Yes, they are supported in all resources today.
Entra objects - May come as new modules if/when the Graph provider will be released which is still in private preview.


How does AVM ensure code quality?

AVM utilizes a number of validation pipelines for both Bicep and Terraform. These pipelines are run on every PR and ensure that the code is compliant with the AVM specifications and that the module is working as expected.

For example, in case of Bicep, as part of the PR process, we’re asking contributors to provide a workflow status badge as a proof of successful validation using our testing pipelines.

The validation includes 2 main stages run in sequence:

  • Static validation: to ensure that the module complies to AVM specifications.
  • Deployment validation: to ensure all test examples are working from a deployment perspective.

These same validations are also run in the BRM repository after merge. The new version of the contributed module is published to the Public Bicep Registry only if all validations are successful.


Does AVM use semantic versioning?

Yes! For generic guidance, see SNFR17 - Semantic Versioning.
For Bicep specific guidance, see BCPNFR14 - Versioning.


What’s the guidance on transitioning to new module versions?

AVM is not different compared to any other solution using semantic versioning.

Customer should consider updating to a newer version of a module if:

  • They need a new feature the new version has introduced.
  • It fixes a bug they were having.
  • They’d like ot use the latest and greatest version.

To do this they just change the version in their module declaration for either Terraform or Bicep and then run it through their pipelines to roll it out.

The high level steps are:

  • Check module documentation for any version-incompatibility notes.
  • Increase the version (point to the selected published version of the module).
  • Do a what-if (Bicep) or terraform plan (Terraform) & review the changes proposed.
    • If all good, proceed to deployment/apply.
    • If not, make required changes to make the plan/what-if as expected.

Using AVM

How can I use Bicep modules through the Public Registry?

Please see the Bicep Quickstart guide here.


Do I need to allow a specific URL to access the Public Registry?

In a regulated environment, network traffic might be limited, especially when using private build agents. The AVM Bicep templates are served from the Microsoft Container Registry. To access this container registry, the URL https://mcr.microsoft.com must be accessible from the network. So, if your network settings or firewall rules prevent access to this URL, you would need to allow it to ensure proper functioning.


Aren’t AVM resource modules too complex for people less skilled in IaC technologies?

TLDR: Resource modules have complexity inside, so they can be flexibly used from the outside.

Resource modules are written in a flexible way; therefore, you don’t need to modify them from project to project, use case to use case, as they aim to cover most of the functionality that a given resource type can provide, in a way that you can interact with any module just by using the required parameters - i.e., you don’t have to know how the template of the particular module works inside, just take a look at the README.md file of the given module to learn how to leverage it.

Resource modules are multi-purpose; therefore, they contain a lot of dynamic expressions (functions, variables, etc.), so there’s no need to maintain multiple instances for different use cases. They can be deployed in different configurations just by changing the input parameters. They should be perceived by the user as black boxes, where they don’t have to worry about the internal complexity of the code, as they only interact with them by their parameters.


Can I call a Bicep child module directly? E.g., can I update or add a secret in an existing Key Vault, or a route in an existing route table?

You can reference Bicep child modules that have been explicitly published to the public bicep registry.

Today, publishing child-modules separately from their parents does not happen by default, and follows an on-demand process. Only child modules explicitly allowed for publishing can be referenced from the registy.
The process is currently in a pilot phase, and documented here.

If not directly, you can still reference child modules via their parents. You can reference e.g. a avm/res/key-vault/vault/key through its parent avm/res/key-vault/vault’s keys parameter. Alternatively, you can grab the module folder locally, although this is a workaround and not recommended.


If I use AVM modules in my solution, do I need to have the MIT license in my own repo also? Do I need to add or reference AVM’s license in my solution?

Microsoft is not in the position of providing legal guidance on what licensing model your product/solution/etc. (the “Software”) leveraging Azure Verified Modules can or should be under. Generally speaking, the MIT license is permissive and allows you to freely use, modify, and distribute the code and does not mandate you to have your entire Software under the MIT license, but you must follow the requirements for the MIT-licensed code that you carry. As stated in the AVM LICENSE reference here, the described “copyright notice and permission notice shall be included in all copies or substantial portions of the Software”.

Glossary

Terms, Abbreviations, and Acronyms

This page holds a table of all the terms, abbreviations, and acronyms that are used across this site.

Term/Abbreviation/AcronymDefinition
AVMAzure Verified Modules
IaCInfrastructure-as-Code
CARMLCommon Azure Resource Modules Library - Azure/ResourceModules
TFVMTerraform Verified Modules - Azure/terrafrom-azure-mdoules
FYMicrosoft Fiscal Year (July to June)
CYCalendar Year (January to December)
CSUMicrosoft Customer Success Unit
ISDMicrosoft Industry Solution Delivery
CSSMicrosoft Customer Services & Support
LZALanding Zone Accelerators - e.g., Azure Virtual Desktop (AVD), Azure VMware Solution (AVS), etc.
RP/RPsAzure Resource Providers
PG/PGsMicrosoft Product Group(s)
AACAzure Architecture Center
WAFWell-Architected Framework
One Microsoft“We are a family of individuals united by a single, shared mission. It’s our ability to work together that makes our dreams believable and, ultimately, achievable. We will build on the ideas of others and collaborate across boundaries to bring the best of Microsoft to our customers as one. We are proud to be part of team Microsoft.”
See Microsoft cultural attributes
FTE/FTEsFull Time Employee(s)
GAGenerally Available
MDFCMicrosoft Defender for Cloud
MCSBMicrosoft Cloud Security Benchmark
MCRMicrosoft Container Registry. Which is what the Bicep Public Registry uses to publish it’s modules via.
PRPull Request
Primary ResourceA primary resource is the main Azure service/product a Resource Module provides / is built around.