Lab 3: Azure Verified Modules (AVM)
Overview
Azure Verified Modules (AVM) represent Microsoft's official library of Infrastructure as Code modules for Azure, providing production-ready, tested, and maintained infrastructure patterns. This module teaches you how to leverage AVM to accelerate your infrastructure deployments while following Microsoft's best practices.
Learning Objectives
- Understand the purpose and benefits of Azure Verified Modules
- Navigate the AVM registry and find appropriate modules
- Implement AVM modules in Terraform configurations
- Configure advanced module parameters for production scenarios
- Understand module versioning and lifecycle management
- Compare AVM modules with custom module development
What are Azure Verified Modules?
1. Definition and Purpose
Azure Verified Modules (AVM) are bicep and Terraform modules that are:
- ✅ Officially supported by Microsoft
- ✅ Production-ready with comprehensive testing
- ✅ Best practice compliant with Azure Well-Architected Framework
- ✅ Continuously maintained with regular updates
- ✅ Security-focused with built-in security configurations
- ✅ Standardized across all Azure services
graph TB
A[Custom Modules] --> D[Development Time]
A --> E[Testing Burden]
A --> F[Maintenance Overhead]
B[Azure Verified Modules] --> G[Instant Productivity]
B --> H[Pre-tested Components]
B --> I[Microsoft Support]
subgraph "Traditional Approach"
A
D
E
F
end
subgraph "AVM Approach"
B
G
H
I
end
style B fill:#2ea44f
style A fill:#f85149
2. AVM vs Traditional Modules
| Aspect | Custom Modules | Azure Verified Modules |
|---|---|---|
| Development Time | Weeks/Months | Minutes/Hours |
| Testing Coverage | Manual testing required | Comprehensive automated testing |
| Security Compliance | Manual implementation | Built-in security best practices |
| Maintenance | Team responsibility | Microsoft maintained |
| Documentation | Team creates | Professional documentation |
| Support | Community/Team | Microsoft official support |
| Updates | Manual tracking | Automated updates available |
| Consistency | Varies by team | Standardized across organization |
3. AVM Module Types
Resource Modules (res-):
- Deploy single Azure resource types
- Example:
Azure/avm-res-storage-storageaccount - Focus: Individual resource configuration
Pattern Modules (ptn-):
- Deploy multiple related resources
- Example:
Azure/avm-ptn-webapp-privateendpoint - Focus: Complete solution patterns
Utility Modules (utl-):
- Helper modules for common tasks
- Example:
Azure/avm-utl-types - Focus: Shared utilities and data types
Navigating the AVM Registry
1. Finding AVM Modules
Terraform Registry Search:
- Go to registry.terraform.io
- Select: Browse modules
- Search for:
Azure/avm - Click on: See all
- Filter by: Provider = azurerm
Module Naming Convention:
Azure/avm-{type}-{category}-{resource}
Examples:
- Azure/avm-res-storage-storageaccount
- Azure/avm-res-web-site
- Azure/avm-ptn-webapp-privateendpoint
- Azure/avm-res-keyvault-vault
Popular AVM Modules:
| Module | Purpose | Use Case |
|---|---|---|
avm-res-storage-storageaccount |
Storage Account | Data storage, static websites |
avm-res-web-site |
App Service | Web applications, APIs |
avm-res-sql-server |
SQL Server | Database servers |
avm-res-keyvault-vault |
Key Vault | Secret management |
avm-res-network-virtualnetwork |
Virtual Network | Network infrastructure |
avm-ptn-webapp-privateendpoint |
Web App + Private Endpoint | Secure web applications |
2. Module Documentation Structure
Each AVM module includes:
README.md:
- Overview and purpose
- Usage examples
- Input/output specifications
- Requirements and dependencies
Examples:
- Complete working examples
- Different configuration scenarios
- Integration patterns
CHANGELOG.md:
- Version history
- Breaking changes
- New features and fixes
Implementing AVM Modules
1. Basic Storage Account Example
Traditional Custom Resource:
resource "azurerm_storage_account" "main" {
name = var.storage_account_name
resource_group_name = var.resource_group_name
location = var.location
account_tier = var.account_tier
account_replication_type = var.replication_type
# Missing: Advanced security, compliance, monitoring
# Missing: Proper tagging, lifecycle management
# Missing: Network security, encryption settings
}
AVM Module Implementation:
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "~> 0.1.0"
# Required parameters
name = var.storage_account_name
resource_group_name = var.resource_group_name
location = var.location
# Advanced configurations included automatically
account_tier = var.account_tier
account_replication_type = var.replication_type
# Security features (enabled by default in AVM)
public_network_access_enabled = false
allow_nested_items_to_be_public = false
# Network access rules
network_rules = {
default_action = "Deny"
ip_rules = var.allowed_ips
virtual_network_subnet_ids = [
data.azurerm_subnet.private.id
]
}
# Encryption configuration
infrastructure_encryption_enabled = true
# Blob properties with versioning
blob_properties = {
versioning_enabled = true
change_feed_enabled = true
delete_retention_policy = {
days = var.environment == "prod" ? 30 : 7
}
container_delete_retention_policy = {
days = var.environment == "prod" ? 30 : 7
}
}
# Private endpoints for secure access
private_endpoints = {
blob = {
private_dns_zone_group_name = "default"
private_dns_zone_resource_group_name = var.dns_resource_group_name
subnet_resource_id = data.azurerm_subnet.private.id
private_service_connection_name = "psc-blob"
is_manual_connection = false
subresource_name = "blob"
private_connection_resource_id = null
private_dns_zone_resource_ids = [
data.azurerm_private_dns_zone.blob.id
]
}
}
# Comprehensive tagging
tags = merge(var.common_tags, {
Component = "Storage"
Service = "DataStorage"
})
}
2. Other examples
Web Application with AVM
# App Service Plan
module "app_service_plan" {
source = "Azure/avm-res-web-serverfarm/azurerm"
version = "~> 0.1.0"
name = "asp-${var.project_name}-${var.environment}"
resource_group_name = var.resource_group_name
location = var.location
sku_name = "P1v2"
os_type = "Linux"
# Auto-scaling for production
maximum_elastic_worker_count = 10
tags = var.common_tags
}
# Web Application
module "web_app" {
source = "Azure/avm-res-web-site/azurerm"
version = "~> 0.1.0"
kind = "webapp"
name = "app-${var.project_name}-${var.environment}"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = module.app_service_plan.resource_id
os_type = "Linux"
application_insights = {
workspace_resource_id = azurerm_log_analytics_workspace.example_production.id
}
auth_settings_v2 = {
default = {
auth_enabled = true
default_provider = "okta"
custom_oidc_v2 = {
default = {
name = "example_oidc_provider"
client_id = "your-client-id"
openid_configuration_endpoint = "<https://test-config-endpoint.com/.well-known/openid-configuration>"
}
}
}
}
enable_telemetry = var.enable_telemetry
site_config = {
}
tags = {
module = "Azure/avm-res-web-site/azurerm"
version = "0.17.2"
}
}
Database with AVM
locals {
databases = {
my_sample_db = {
name = "my_sample_db"
create_mode = "Default"
collation = "SQL_Latin1_General_CP1_CI_AS"
license_type = "LicenseIncluded"
max_size_gb = 50
sku_name = "S0"
short_term_retention_policy = {
retention_days = 1
backup_interval_in_hours = 24
}
long_term_retention_policy = {
weekly_retention = "P2W1D"
monthly_retention = "P2M"
yearly_retention = "P1Y"
week_of_year = 1
}
tags = local.tags
}
}
tags = {
environment = "sample"
cost_centre = "demo"
}
}
module "sql_server" {
source = "Azure/avm-res-sql-server/azurerm"
version = "~> 0.1.0"
name = "sql-${var.project_name}-${var.environment}"
location = var.location
resource_group_name = var.resource_group_name
server_version = "12.0"
administrator_login = "mysqladmin"
administrator_login_password = random_password.admin_password.result
private_endpoints = {
primary = {
private_dns_zone_resource_ids = [
azurerm_private_dns_zone.this.id
]
subnet_resource_id = azurerm_subnet.this.id
subresource_name = "sqlServer"
}
}
public_network_access_enabled = false
databases = local.databases
enable_telemetry = var.enable_telemetry
}
Keyvault and Secrets with AVM
provider "azurerm" {
features {}
}
terraform {
required_version = ">= 1.9, < 2.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.117"
}
http = {
source = "hashicorp/http"
version = "~> 3.4"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}
module "regions" {
source = "Azure/avm-utl-regions/azurerm"
version = "0.3.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 we 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 = "../../"
location = azurerm_resource_group.this.location
# source = "Azure/avm-res-keyvault-vault/azurerm"
name = module.naming.key_vault.name_unique
resource_group_name = azurerm_resource_group.this.name
tenant_id = data.azurerm_client_config.current.tenant_id
enable_telemetry = var.enable_telemetry
network_acls = {
bypass = "AzureServices"
ip_rules = ["${data.http.ip.response_body}/32"]
}
public_network_access_enabled = true
role_assignments = {
deployment_user_kv_admin = {
role_definition_id_or_name = "Key Vault Administrator"
principal_id = data.azurerm_client_config.current.object_id
}
}
secrets = {
test_secret = {
name = "test-secret"
}
}
secrets_value = {
test_secret = "secret-value"
}
wait_for_rbac_before_secret_operations = {
create = "60s"
}
}
Advanced AVM Configuration
1. Pattern Module Example - Azure Landing Zone + Management (Azure Automation Account)
provider "alz" {
library_references = [{
path = "platform/alz"
ref = "2024.10.1"
}]
}
provider "azurerm" {
features {}
}
data "azapi_client_config" "current" {}
locals {
automation_account_name = "aa-${var.random_suffix}"
location = "swedencentral"
log_analytics_workspace_name = "law-${var.random_suffix}"
resource_group_name = "rg-alz-management-${var.random_suffix}"
uami_name = "uami-ama"
}
module "management" {
source = "Azure/avm-ptn-alz-management/azurerm"
version = "0.6.0"
automation_account_name = local.automation_account_name
location = local.location
log_analytics_workspace_name = local.log_analytics_workspace_name
resource_group_name = local.resource_group_name
}
module "alz" {
source = "Azure/avm-ptn-alz/azurerm"
architecture_name = "alz"
location = local.location
parent_resource_id = data.azapi_client_config.current.tenant_id
dependencies = {
policy_assignments = [
module.management.data_collection_rule_ids,
module.management.resource_id,
module.management.user_assigned_identity_ids,
]
}
policy_assignments_to_modify = {
connectivity = {
policy_assignments = {
# As we don't have a DDOS protection plan, we need to disable this policy
# to prevent a modify action from failing.
Enable-DDoS-VNET = {
enforcement_mode = "DoNotEnforce"
}
}
}
}
policy_default_values = {
ama_change_tracking_data_collection_rule_id = jsonencode({ value = provider::azapi::resource_group_resource_id(data.azapi_client_config.current.subscription_id, local.resource_group_name, "Microsoft.Insights/dataCollectionRules", ["dcr-change-tracking"]) })
ama_mdfc_sql_data_collection_rule_id = jsonencode({ value = provider::azapi::resource_group_resource_id(data.azapi_client_config.current.subscription_id, local.resource_group_name, "Microsoft.Insights/dataCollectionRules", ["dcr-defender-sql"]) })
ama_vm_insights_data_collection_rule_id = jsonencode({ value = provider::azapi::resource_group_resource_id(data.azapi_client_config.current.subscription_id, local.resource_group_name, "Microsoft.Insights/dataCollectionRules", ["dcr-vm-insights"]) })
ama_user_assigned_managed_identity_id = jsonencode({ value = provider::azapi::resource_group_resource_id(data.azapi_client_config.current.subscription_id, local.resource_group_name, "Microsoft.ManagedIdentity/userAssignedIdentities", [local.uami_name]) })
ama_user_assigned_managed_identity_name = jsonencode({ value = local.uami_name })
log_analytics_workspace_id = jsonencode({ value = provider::azapi::resource_group_resource_id(data.azapi_client_config.current.subscription_id, local.resource_group_name, "Microsoft.OperationalInsights/workspaces", [local.log_analytics_workspace_name]) })
}
}
Module Versioning and Updates
1. Version Constraints
Semantic Versioning:
# Exact version (not recommended for production)
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "0.1.5"
}
# Compatible version (recommended)
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "~> 0.1.0" # >= 0.1.0, < 0.2.0
}
# Minimum version
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = ">= 0.1.0"
}
2. Update Strategy
Development Environment:
# Update providers and modules
terraform init -upgrade
# Review what will change
terraform plan
# Test updates in development first
terraform apply -var-file="environments/dev/terraform.tfvars"
Production Environment:
Update gradually:
- Update in dev environment:
-var-file="environments/dev/terraform.tfvars" - Test thoroughly
- Update staging environment:
-var-file="environments/staging/terraform.tfvars" - Production update during maintenance window
Pin to specific versions for stability: version = "0.1.5"
3. Breaking Changes
Example Breaking Change:
# Version 0.1.x
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "~> 0.1.0"
# Old parameter name
account_replication_type = "LRS"
}
# Version 0.2.x (breaking change)
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "~> 0.2.0"
# New parameter structure
redundancy = {
type = "LRS"
}
}
AVM vs Custom Modules Comparison
When to Use AVM
✅ Use AVM When:
- Standard Azure resource deployment needed
- Security and compliance requirements are standard
- Team prefers Microsoft-supported solutions
- Rapid deployment is priority
- Limited Terraform expertise on team
- Consistent patterns across organization desired
Example Scenarios:
# Perfect for AVM - Standard web application
module "web_app" {
source = "Azure/avm-res-web-site/azurerm"
version = "~> 0.1.0"
name = var.app_name
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = module.app_service_plan.resource_id
}
When to Create Custom Modules
✅ Use Custom Modules When:
- Highly specific organizational requirements
- AVM module doesn't exist for your use case
- Need custom business logic or integrations
- Existing custom modules work well
- Team has strong Terraform expertise
Example Scenarios:
# Better as custom module - Complex multi-tenant application
module "multi_tenant_app" {
source = "./modules/multi-tenant-app"
tenants = {
tenant1 = {
database_size = "S1"
features = ["feature_a", "feature_b"]
custom_domain = "tenant1.company.com"
}
tenant2 = {
database_size = "S2"
features = ["feature_a", "feature_c"]
custom_domain = "tenant2.company.com"
}
}
}
Hybrid Approach
Best Practice: Combine AVM and custom modules:
# Use AVM for standard infrastructure
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "~> 0.1.0"
name = var.storage_account_name
resource_group_name = var.resource_group_name
location = var.location
}
module "key_vault" {
source = "Azure/avm-res-keyvault-vault/azurerm"
version = "~> 0.1.0"
name = var.key_vault_name
resource_group_name = var.resource_group_name
location = var.location
}
# Use custom module for specialized business logic
module "application_deployment" {
source = "./modules/application-deployment"
application_config = var.application_config
storage_account_id = module.storage_account.resource_id
key_vault_id = module.key_vault.resource_id
# Custom business logic
tenant_configurations = var.tenant_configurations
feature_flags = var.feature_flags
custom_integrations = var.custom_integrations
}
Practical Exercise: Migrate to AVM
Objective
Take a resource of the previous lab and configure it with AVM instead of azurerm_<resource> method.
Before: Custom Configuration
resource "azurerm_storage_account" "main" {
name = "st${replace(local.resource_prefix, "-", "")}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = var.storage_replication_type
account_kind = "StorageV2"
# Security settings
public_network_access_enabled = false
allow_nested_items_to_be_public = false
shared_access_key_enabled = true
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
# Infrastructure encryption for production
infrastructure_encryption_enabled = local.environment_config.is_production
# Blob properties
blob_properties {
# Enable versioning
versioning_enabled = true
# Change feed for audit trail
change_feed_enabled = local.environment_config.is_production
# Retention policy
delete_retention_policy {
days = var.storage_blob_retention_days
}
# Container retention policy
container_delete_retention_policy {
days = var.storage_blob_retention_days
}
}
# Network rules (restrictive by default)
network_rules {
default_action = "Deny"
ip_rules = [""] # Add your IP addresses as needed
# Allow access from the same virtual network (when implemented)
virtual_network_subnet_ids = []
}
tags = merge(local.common_tags, {
Component = "Storage"
Service = "DataStorage"
})
}
After: AVM Implementation
module "storage_account_main" {
source = "Azure/avm-res-storage-storageaccount/azurerm"
version = "0.6.4"
location = azurerm_resource_group.main.location
name = "st${replace(local.resource_prefix, "-", "")}"
resource_group_name = azurerm_resource_group.main.name
account_kind = "StorageV2"
account_replication_type = var.storage_replication_type
account_tier = "Standard"
# Security settings
public_network_access_enabled = false
# allow_nested_items_to_be_public = false
shared_access_key_enabled = true
https_traffic_only_enabled = true
min_tls_version = "TLS1_2"
# Infrastructure encryption for production
infrastructure_encryption_enabled = local.environment_config.is_production
# Blob properties
blob_properties = {
# Enable versioning
versioning_enabled = true
# Change feed for audit trail
change_feed_enabled = local.environment_config.is_production
# Retention policy
delete_retention_policy = {
days = var.storage_blob_retention_days
}
# Container retention policy
container_delete_retention_policy = {
days = var.storage_blob_retention_days
}
}
# Network rules (restrictive by default)
network_rules = {
bypass = ["AzureServices"]
default_action = "Deny"
ip_rules = [""] # Add your IP addresses
virtual_network_subnet_ids = []
}
tags = merge(local.common_tags, {
Component = "Storage"
Service = "DataStorage"
})
}
Test and move existing resource
It's time to test your newer implementation. Plan and apply your changes on the dev environment to ensure everything works as expected.
You may encounter some warnings or errors like:
- Dependencies to old resource: Update dependencies to rely on the newer module.
- While moving to AVM, some outputs of the previous resource are not available anymore in the module outputs.
- Consider the
module.xxxxx.resourceas the new entrypoint to look for your output
- Consider the
- Terraform is willing to replace your old resource with a new one that you may want to keep.
- Consider using
terraform state mv - Ex for a storage account:
terraform state mv azurerm_storage_account.main module.storage_account_main.azurerm_storage_account.this
- Consider using
Migration Benefits Analysis
| Aspect | Custom Module | AVM Module |
|---|---|---|
| Security Features | Basic | Comprehensive |
| Configuration Options | Limited | Extensive |
| Maintenance Effort | High | None |
| Testing Coverage | Manual | Automated |
| Documentation | Variable | Professional |
Best Practices with AVM
1. Module Selection
# ✅ GOOD: Use appropriate module type
module "storage_account" {
source = "Azure/avm-res-storage-storageaccount/azurerm" # Resource module
version = "~> 0.1.0"
}
module "webapp_with_networking" {
source = "Azure/avm-ptn-webapp-privateendpoint/azurerm" # Pattern module
version = "~> 0.1.0"
}
2. Configuration Management
# ✅ GOOD: Environment-specific configurations
module "web_app" {
source = "Azure/avm-res-web-site/azurerm"
version = "~> 0.1.0"
name = "app-${var.project_name}-${var.environment}"
resource_group_name = var.resource_group_name
location = var.location
service_plan_id = module.app_service_plan.resource_id
# Environment-specific configuration
site_config = {
always_on = var.environment == "prod"
application_stack = {
node_version = "18-lts"
}
# Production-only features
health_check_path = var.environment == "prod" ? "/health" : null
}
# Environment-specific scaling
tags = merge(var.common_tags, {
Environment = var.environment
Criticality = var.environment == "prod" ? "High" : "Medium"
})
}
3. Output Management
# ✅ GOOD: Expose relevant outputs
output "web_app_url" {
description = "URL of the web application"
value = "https://${module.web_app.default_hostname}"
}
output "storage_account_name" {
description = "Name of the storage account"
value = module.storage_account.name
}
output "key_vault_uri" {
description = "URI of the Key Vault"
value = module.key_vault.vault_uri
}
Next Steps
After mastering Azure Verified Modules:
- Explore Module Catalog: Browse all available AVM modules
- Contribute Back: Consider contributing to AVM project
- Automation: Integrate AVM modules into CI/CD pipelines
- Monitoring: Set up update notifications for modules
- Documentation: Document your AVM usage patterns
This completes the Azure Verified Modules module. You now understand how to leverage Microsoft's official infrastructure modules to accelerate your deployments while following best practices.
Workshop Conclusion
🎉 Congratulations! You've completed the Practical IaC—Terraform & Azure Modules Workshop!
Thank you for participating! Questions? 🤔