From 091d3b7e1259d7783632709e2a004e85653d6e5d Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 14:50:28 -0700 Subject: [PATCH 01/14] Add AzAPI sample: 101-azure-virtual-desktop-azapi Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../afstorage.tf | 52 ++++++ .../101-azure-virtual-desktop-azapi/host.tf | 159 ++++++++++++++++++ .../loganalytics.tf | 21 +++ .../101-azure-virtual-desktop-azapi/main.tf | 73 ++++++++ .../networking.tf | 110 ++++++++++++ .../outputs.tf | 54 ++++++ .../providers.tf | 28 +++ .../101-azure-virtual-desktop-azapi/rbac.tf | 26 +++ .../101-azure-virtual-desktop-azapi/sig.tf | 49 ++++++ .../variables.tf | 148 ++++++++++++++++ 10 files changed, 720 insertions(+) create mode 100644 quickstart/101-azure-virtual-desktop-azapi/afstorage.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/host.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/main.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/networking.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/outputs.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/providers.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/rbac.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/sig.tf create mode 100644 quickstart/101-azure-virtual-desktop-azapi/variables.tf diff --git a/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf b/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf new file mode 100644 index 000000000..a736764fd --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf @@ -0,0 +1,52 @@ +# Create a Resource Group for Storage +resource "azurerm_resource_group" "rg_storage" { + location = var.deploy_location + name = var.rg_stor +} + +# Generate a random string (consisting of four characters) +resource "random_string" "random" { + length = 4 + upper = false + special = false +} + +# Create a File Storage Account using AzAPI +resource "azapi_resource" "storage" { + type = "Microsoft.Storage/storageAccounts@2023-05-01" + name = "stor${random_string.random.id}" + location = azurerm_resource_group.rg_storage.location + parent_id = azurerm_resource_group.rg_storage.id + + body = { + kind = "FileStorage" + sku = { + name = "Premium_LRS" + } + properties = {} + } + + response_export_values = ["properties", "id"] +} + +# Create File Share using AzAPI +resource "azapi_resource" "fs_share" { + type = "Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01" + name = "fslogix" + parent_id = "${azapi_resource.storage.id}/fileServices/default" + + body = { + properties = {} + } +} + +# Azure built-in roles - keep as azurerm +data "azurerm_role_definition" "storage_role" { + name = "Storage File Data SMB Share Contributor" +} + +resource "azurerm_role_assignment" "af_role" { + scope = azapi_resource.storage.id + role_definition_id = data.azurerm_role_definition.storage_role.id + principal_id = azuread_group.aad_group.id +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf new file mode 100644 index 000000000..befa73eaa --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -0,0 +1,159 @@ +locals { + registration_token = azurerm_virtual_desktop_host_pool_registration_info.registrationinfo.token +} + +resource "random_string" "AVD_local_password" { + count = var.rdsh_count + length = 16 + special = true + min_special = 2 + override_special = "*!@#?" +} + +resource "azurerm_resource_group" "rg" { + name = var.rg + location = var.resource_group_location +} + +# Create Network Interfaces using AzAPI +resource "azapi_resource" "avd_vm_nic" { + count = var.rdsh_count + type = "Microsoft.Network/networkInterfaces@2024-01-01" + name = "${var.prefix}-${count.index + 1}-nic" + location = azurerm_resource_group.rg.location + parent_id = azurerm_resource_group.rg.id + + body = { + properties = { + ipConfigurations = [ + { + name = "nic${count.index + 1}_config" + properties = { + privateIPAllocationMethod = "Dynamic" + subnet = { + id = jsondecode(azapi_resource.vnet.output).properties.subnets[0].id + } + } + } + ] + } + } +} + +# Create Windows Virtual Machines using AzAPI +resource "azapi_resource" "avd_vm" { + count = var.rdsh_count + type = "Microsoft.Compute/virtualMachines@2024-03-01" + name = "${var.prefix}-${count.index + 1}" + location = azurerm_resource_group.rg.location + parent_id = azurerm_resource_group.rg.id + + body = { + properties = { + hardwareProfile = { + vmSize = var.vm_size + } + osProfile = { + computerName = "${var.prefix}-${count.index + 1}" + adminUsername = var.local_admin_username + adminPassword = var.local_admin_password + windowsConfiguration = { + provisionVMAgent = true + } + } + storageProfile = { + osDisk = { + name = "${lower(var.prefix)}-${count.index + 1}" + caching = "ReadWrite" + createOption = "FromImage" + managedDisk = { + storageAccountType = "Standard_LRS" + } + } + imageReference = { + publisher = "MicrosoftWindowsDesktop" + offer = "Windows-10" + sku = "20h2-evd" + version = "latest" + } + } + networkProfile = { + networkInterfaces = [ + { + id = azapi_resource.avd_vm_nic[count.index].id + } + ] + } + } + } + + depends_on = [azapi_resource.avd_vm_nic] +} + +# Domain Join Extension using AzAPI +resource "azapi_resource" "domain_join" { + count = var.rdsh_count + type = "Microsoft.Compute/virtualMachines/extensions@2024-03-01" + name = "${var.prefix}-${count.index + 1}-domainJoin" + location = azurerm_resource_group.rg.location + parent_id = azapi_resource.avd_vm[count.index].id + + body = { + properties = { + publisher = "Microsoft.Compute" + type = "JsonADDomainExtension" + typeHandlerVersion = "1.3" + autoUpgradeMinorVersion = true + settings = { + Name = var.domain_name + OUPath = var.ou_path + User = "${var.domain_user_upn}@${var.domain_name}" + Restart = "true" + Options = "3" + } + protectedSettings = { + Password = var.domain_password + } + } + } + + depends_on = [ + azapi_resource.peer1, + azapi_resource.peer2 + ] +} + +# DSC Extension for AVD agent using AzAPI +resource "azapi_resource" "vmext_dsc" { + count = var.rdsh_count + type = "Microsoft.Compute/virtualMachines/extensions@2024-03-01" + name = "${var.prefix}${count.index + 1}-avd_dsc" + location = azurerm_resource_group.rg.location + parent_id = azapi_resource.avd_vm[count.index].id + + body = { + properties = { + publisher = "Microsoft.Powershell" + type = "DSC" + typeHandlerVersion = "2.73" + autoUpgradeMinorVersion = true + settings = { + modulesUrl = "https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_1.0.02714.342.zip" + configurationFunction = "Configuration.ps1\\AddSessionHost" + properties = { + HostPoolName = azurerm_virtual_desktop_host_pool.hostpool.name + } + } + protectedSettings = { + properties = { + registrationInfoToken = local.registration_token + } + } + } + } + + depends_on = [ + azapi_resource.domain_join, + azurerm_virtual_desktop_host_pool.hostpool + ] +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf b/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf new file mode 100644 index 000000000..bfd2fafa7 --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf @@ -0,0 +1,21 @@ +resource "azurerm_resource_group" "log" { + name = var.rg_shared_name + location = var.deploy_location +} + +# Creates Log Analytics Workspace using AzAPI +resource "azapi_resource" "law" { + type = "Microsoft.OperationalInsights/workspaces@2023-09-01" + name = "log${random_string.random.id}" + location = azurerm_resource_group.log.location + parent_id = azurerm_resource_group.log.id + + body = { + properties = { + sku = { + name = "PerGB2018" + } + retentionInDays = 30 + } + } +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/main.tf b/quickstart/101-azure-virtual-desktop-azapi/main.tf new file mode 100644 index 000000000..a57aebe41 --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/main.tf @@ -0,0 +1,73 @@ +# Resource group for AVD service objects +resource "azurerm_resource_group" "sh" { + name = var.rg_name + location = var.resource_group_location +} + +# Create AVD workspace using AzAPI +resource "azapi_resource" "workspace" { + type = "Microsoft.DesktopVirtualization/workspaces@2024-04-03" + name = var.workspace + location = azurerm_resource_group.sh.location + parent_id = azurerm_resource_group.sh.id + + body = { + properties = { + friendlyName = "${var.prefix} Workspace" + description = "${var.prefix} Workspace" + } + } +} + +# Create AVD host pool - keep as azurerm (registration token not easily available via azapi) +resource "azurerm_virtual_desktop_host_pool" "hostpool" { + resource_group_name = azurerm_resource_group.sh.name + location = azurerm_resource_group.sh.location + name = var.hostpool + friendly_name = var.hostpool + validate_environment = true + custom_rdp_properties = "audiocapturemode:i:1;audiomode:i:0;" + description = "${var.prefix} Terraform HostPool" + type = "Pooled" + maximum_sessions_allowed = 16 + load_balancer_type = "DepthFirst" +} + +resource "azurerm_virtual_desktop_host_pool_registration_info" "registrationinfo" { + hostpool_id = azurerm_virtual_desktop_host_pool.hostpool.id + expiration_date = var.rfc3339 +} + +# Create AVD Application Group using AzAPI +resource "azapi_resource" "dag" { + type = "Microsoft.DesktopVirtualization/applicationGroups@2024-04-03" + name = "${var.prefix}-dag" + location = azurerm_resource_group.sh.location + parent_id = azurerm_resource_group.sh.id + + body = { + properties = { + friendlyName = "Desktop AppGroup" + description = "AVD application group" + hostPoolArmPath = azurerm_virtual_desktop_host_pool.hostpool.id + applicationGroupType = "Desktop" + } + } + + depends_on = [azurerm_virtual_desktop_host_pool.hostpool, azapi_resource.workspace] +} + +# Associate Workspace and DAG - update workspace with application group reference +resource "azapi_update_resource" "ws_dag_association" { + type = "Microsoft.DesktopVirtualization/workspaces@2024-04-03" + name = var.workspace + parent_id = azurerm_resource_group.sh.id + + body = { + properties = { + applicationGroupReferences = [azapi_resource.dag.id] + } + } + + depends_on = [azapi_resource.workspace, azapi_resource.dag] +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/networking.tf b/quickstart/101-azure-virtual-desktop-azapi/networking.tf new file mode 100644 index 000000000..461b98ed8 --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/networking.tf @@ -0,0 +1,110 @@ +# Create Virtual Network using AzAPI +resource "azapi_resource" "vnet" { + type = "Microsoft.Network/virtualNetworks@2024-01-01" + name = "${var.prefix}-VNet" + location = var.deploy_location + parent_id = azurerm_resource_group.rg.id + + body = { + properties = { + addressSpace = { + addressPrefixes = var.vnet_range + } + dhcpOptions = { + dnsServers = var.dns_servers + } + subnets = [ + { + name = "default" + properties = { + addressPrefix = var.subnet_range[0] + } + } + ] + } + } + + response_export_values = ["properties.subnets"] +} + +# Create Network Security Group using AzAPI +resource "azapi_resource" "nsg" { + type = "Microsoft.Network/networkSecurityGroups@2024-01-01" + name = "${var.prefix}-NSG" + location = var.deploy_location + parent_id = azurerm_resource_group.rg.id + + body = { + properties = { + securityRules = [ + { + name = "HTTPS" + properties = { + priority = 1001 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + sourcePortRange = "*" + destinationPortRange = "443" + sourceAddressPrefix = "*" + destinationAddressPrefix = "*" + } + } + ] + } + } +} + +# Associate NSG with subnet +resource "azapi_update_resource" "nsg_assoc" { + type = "Microsoft.Network/virtualNetworks/subnets@2024-01-01" + name = "default" + parent_id = azapi_resource.vnet.id + + body = { + properties = { + addressPrefix = var.subnet_range[0] + networkSecurityGroup = { + id = azapi_resource.nsg.id + } + } + } + + depends_on = [azapi_resource.vnet, azapi_resource.nsg] +} + +# Data source to get existing AD VNet +data "azurerm_virtual_network" "ad_vnet_data" { + name = var.ad_vnet + resource_group_name = var.ad_rg +} + +# VNet peering: AVD spoke -> AD +resource "azapi_resource" "peer1" { + type = "Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-01-01" + name = "peer_avdspoke_ad" + parent_id = azapi_resource.vnet.id + + body = { + properties = { + remoteVirtualNetwork = { + id = data.azurerm_virtual_network.ad_vnet_data.id + } + } + } +} + +# VNet peering: AD -> AVD spoke +resource "azapi_resource" "peer2" { + type = "Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-01-01" + name = "peer_ad_avdspoke" + parent_id = "${data.azurerm_virtual_network.ad_vnet_data.id}" + + body = { + properties = { + remoteVirtualNetwork = { + id = azapi_resource.vnet.id + } + } + } +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/outputs.tf b/quickstart/101-azure-virtual-desktop-azapi/outputs.tf new file mode 100644 index 000000000..657abae3a --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/outputs.tf @@ -0,0 +1,54 @@ +output "azure_virtual_desktop_compute_resource_group" { + description = "Name of the Resource group in which to deploy session host" + value = azurerm_resource_group.rg.name +} + +output "azure_virtual_desktop_host_pool" { + description = "Name of the Azure Virtual Desktop host pool" + value = azurerm_virtual_desktop_host_pool.hostpool.name +} + +output "azurerm_virtual_desktop_application_group" { + description = "Name of the Azure Virtual Desktop DAG" + value = azapi_resource.dag.name +} + +output "azurerm_virtual_desktop_workspace" { + description = "Name of the Azure Virtual Desktop workspace" + value = azapi_resource.workspace.name +} + +output "location" { + description = "The Azure region" + value = azurerm_resource_group.rg.location +} + +output "storage_account" { + description = "Storage account for Profiles" + value = azapi_resource.storage.name +} + +output "storage_account_share" { + description = "Name of the Azure File Share created for FSLogix" + value = azapi_resource.fs_share.name +} + +output "session_host_count" { + description = "The number of VMs created" + value = var.rdsh_count +} + +output "dnsservers" { + description = "Custom DNS configuration" + value = var.dns_servers +} + +output "vnetrange" { + description = "Address range for deployment vnet" + value = var.vnet_range +} + +output "AVD_user_groupname" { + description = "Azure Active Directory Group for AVD users" + value = azuread_group.aad_group.display_name +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/providers.tf b/quickstart/101-azure-virtual-desktop-azapi/providers.tf new file mode 100644 index 000000000..05231b4b4 --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/providers.tf @@ -0,0 +1,28 @@ +terraform { + required_version = ">=1.0" + + required_providers { + azapi = { + source = "Azure/azapi" + version = "~>2.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~>3.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~>2.0" + } + random = { + source = "hashicorp/random" + version = "~>3.0" + } + } +} + +provider "azapi" {} + +provider "azurerm" { + features {} +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/rbac.tf b/quickstart/101-azure-virtual-desktop-azapi/rbac.tf new file mode 100644 index 000000000..9592e4976 --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/rbac.tf @@ -0,0 +1,26 @@ +# RBAC resources kept as azurerm/azuread - azapi doesn't manage RBAC well +data "azuread_user" "aad_user" { + for_each = toset(var.avd_users) + user_principal_name = format("%s", each.key) +} + +data "azurerm_role_definition" "role" { + name = "Desktop Virtualization User" +} + +resource "azuread_group" "aad_group" { + display_name = var.aad_group_name + security_enabled = true +} + +resource "azuread_group_member" "aad_group_member" { + for_each = data.azuread_user.aad_user + group_object_id = azuread_group.aad_group.id + member_object_id = each.value["id"] +} + +resource "azurerm_role_assignment" "role" { + scope = azapi_resource.dag.id + role_definition_id = data.azurerm_role_definition.role.id + principal_id = azuread_group.aad_group.id +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/sig.tf b/quickstart/101-azure-virtual-desktop-azapi/sig.tf new file mode 100644 index 000000000..97854441f --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/sig.tf @@ -0,0 +1,49 @@ +resource "azurerm_resource_group" "sigrg" { + location = var.deploy_location + name = var.rg_shared_name +} + +resource "random_string" "rando" { + length = 4 + upper = false + special = false +} + +# Creates Shared Image Gallery using AzAPI +resource "azapi_resource" "sig" { + type = "Microsoft.Compute/galleries@2022-03-03" + name = "sig${random_string.random.id}" + location = azurerm_resource_group.sigrg.location + parent_id = azurerm_resource_group.sigrg.id + + body = { + properties = { + description = "Shared images" + } + } + + tags = { + Environment = "Demo" + Tech = "Terraform" + } +} + +# Creates image definition using AzAPI +resource "azapi_resource" "sig_image" { + type = "Microsoft.Compute/galleries/images@2022-03-03" + name = "avd-image" + location = azurerm_resource_group.sigrg.location + parent_id = azapi_resource.sig.id + + body = { + properties = { + osType = "Windows" + osState = "Generalized" + identifier = { + publisher = "MicrosoftWindowsDesktop" + offer = "office-365" + sku = "20h2-evd-o365pp" + } + } + } +} diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf new file mode 100644 index 000000000..7dd8ed4d4 --- /dev/null +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -0,0 +1,148 @@ +variable "resource_group_location" { + default = "eastus" + description = "Location of the resource group." +} + +variable "rg" { + type = string + default = "rg-avd-compute" + description = "Name of the Resource group in which to deploy session host" +} + +variable "rg_name" { + type = string + default = "rg-avd-resources" + description = "Name of the Resource group in which to deploy service objects" +} + +variable "rg_stor" { + type = string + default = "rg-avd-storage" + description = "Name of the Resource group in which to deploy storage" +} + +variable "rg_shared_name" { + type = string + default = "rg-shared-resources" + description = "Name of the Resource group in which to deploy shared resources" +} + +variable "deploy_location" { + type = string + default = "eastus" + description = "The Azure Region in which all resources in this example should be created." +} + +variable "workspace" { + type = string + description = "Name of the Azure Virtual Desktop workspace" + default = "AVD TF Workspace" +} + +variable "hostpool" { + type = string + description = "Name of the Azure Virtual Desktop host pool" + default = "AVD-TF-HP" +} + +variable "ad_vnet" { + type = string + default = "infra-network" + description = "Name of domain controller vnet" +} + +variable "rfc3339" { + type = string + default = "2022-03-30T12:43:13Z" + description = "Registration token expiration" +} + +variable "dns_servers" { + type = list(string) + default = ["10.0.1.4", "168.63.129.16"] + description = "Custom DNS configuration" +} + +variable "vnet_range" { + type = list(string) + default = ["10.2.0.0/16"] + description = "Address range for deployment VNet" +} + +variable "subnet_range" { + type = list(string) + default = ["10.2.0.0/24"] + description = "Address range for session host subnet" +} + +variable "ad_rg" { + type = string + default = "infra-rg" + description = "The resource group for AD VM" +} + +variable "avd_users" { + description = "AVD users" + default = [ + "avduser01@contoso.net", + "avduser02@contoso.net" + ] +} + +variable "aad_group_name" { + type = string + default = "AVDUsers" + description = "Azure Active Directory Group for AVD users" +} + +variable "rdsh_count" { + description = "Number of AVD machines to deploy" + default = 2 +} + +variable "prefix" { + type = string + default = "avdtf" + description = "Prefix of the name of the AVD machine(s)" +} + +variable "domain_name" { + type = string + default = "infra.local" + description = "Name of the domain to join" +} + +variable "domain_user_upn" { + type = string + default = "domainjoineruser" + description = "Username for domain join (do not include domain name as this is appended)" +} + +variable "domain_password" { + type = string + default = "ChangeMe123!" + description = "Password of the user to authenticate with the domain" + sensitive = true +} + +variable "vm_size" { + description = "Size of the machine to deploy" + default = "Standard_DS2_v2" +} + +variable "ou_path" { + default = "" +} + +variable "local_admin_username" { + type = string + default = "localadm" + description = "local admin username" +} + +variable "local_admin_password" { + type = string + default = "ChangeMe123!" + description = "local admin password" + sensitive = true +} From 50177411b280a003fbcfe521dafaab6fec599b5d Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 14:51:38 -0700 Subject: [PATCH 02/14] Add AzAPI sample: 101-cosmos-db-azure-container-instance-azapi Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../aci.tf | 67 +++++++++++++++++++ .../main.tf | 41 ++++++++++++ .../outputs.tf | 11 +++ .../providers.tf | 28 ++++++++ .../variables.tf | 10 +++ 5 files changed, 157 insertions(+) create mode 100644 quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf create mode 100644 quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf create mode 100644 quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf create mode 100644 quickstart/101-cosmos-db-azure-container-instance-azapi/providers.tf create mode 100644 quickstart/101-cosmos-db-azure-container-instance-azapi/variables.tf diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf new file mode 100644 index 000000000..e5bc7520d --- /dev/null +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf @@ -0,0 +1,67 @@ +# Create Container Instance using AzAPI +resource "azapi_resource" "main" { + type = "Microsoft.ContainerInstance/containerGroups@2023-05-01" + name = "${random_pet.rg_name.id}-vote-aci" + location = azurerm_resource_group.rg.location + parent_id = azurerm_resource_group.rg.id + + body = { + properties = { + osType = "Linux" + ipAddress = { + type = "Public" + dnsNameLabel = "vote-aci-${random_integer.ri.result}" + ports = [ + { + port = 80 + protocol = "TCP" + } + ] + } + containers = [ + { + name = "vote-aci" + properties = { + image = "mcr.microsoft.com/azuredocs/azure-vote-front:cosmosdb" + resources = { + requests = { + cpu = 0.5 + memoryInGB = 1.5 + } + } + ports = [ + { + port = 80 + protocol = "TCP" + } + ] + environmentVariables = [ + { + name = "COSMOS_DB_ENDPOINT" + secureValue = jsondecode(azapi_resource.vote_cosmos_db.output).properties.documentEndpoint + }, + { + name = "COSMOS_DB_MASTERKEY" + secureValue = jsondecode(azapi_resource.vote_cosmos_db.output).properties.primaryMasterKey + }, + { + name = "TITLE" + value = "Azure Voting App" + }, + { + name = "VOTE1VALUE" + value = "Cats" + }, + { + name = "VOTE2VALUE" + value = "Dogs" + } + ] + } + } + ] + } + } + + response_export_values = ["properties.ipAddress.fqdn"] +} diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf new file mode 100644 index 000000000..8d873aab9 --- /dev/null +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf @@ -0,0 +1,41 @@ +resource "azurerm_resource_group" "rg" { + name = "${random_pet.rg_name.id}-rg" + location = var.resource_group_location +} + +# Create Cosmos DB Account using AzAPI +resource "azapi_resource" "vote_cosmos_db" { + type = "Microsoft.DocumentDB/databaseAccounts@2024-05-15" + name = "${random_pet.rg_name.id}-${random_integer.ri.result}" + location = azurerm_resource_group.rg.location + parent_id = azurerm_resource_group.rg.id + + body = { + kind = "GlobalDocumentDB" + properties = { + databaseAccountOfferType = "Standard" + consistencyPolicy = { + defaultConsistencyLevel = "BoundedStaleness" + maxIntervalInSeconds = 10 + maxStalenessPrefix = 200 + } + locations = [ + { + locationName = azurerm_resource_group.rg.location + failoverPriority = 0 + } + ] + } + } + + response_export_values = ["properties.documentEndpoint", "properties.primaryMasterKey"] +} + +resource "random_integer" "ri" { + min = 10000 + max = 99999 +} + +resource "random_pet" "rg_name" { + prefix = var.prefix +} diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf new file mode 100644 index 000000000..113d4637a --- /dev/null +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf @@ -0,0 +1,11 @@ +output "resource_group_name" { + value = azurerm_resource_group.rg.name +} + +output "cosmosdb_account_name" { + value = azapi_resource.vote_cosmos_db.name +} + +output "dns" { + value = jsondecode(azapi_resource.main.output).properties.ipAddress.fqdn +} diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/providers.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/providers.tf new file mode 100644 index 000000000..00c5b141d --- /dev/null +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/providers.tf @@ -0,0 +1,28 @@ +terraform { + required_version = ">=1.0" + + required_providers { + azapi = { + source = "Azure/azapi" + version = "~>2.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~>3.0" + } + random = { + source = "hashicorp/random" + version = "~>3.0" + } + } +} + +provider "azapi" {} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/variables.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/variables.tf new file mode 100644 index 000000000..5172f6d2a --- /dev/null +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/variables.tf @@ -0,0 +1,10 @@ +variable "resource_group_location" { + default = "East Asia" + description = "Location of the resource group." +} + +variable "prefix" { + type = string + default = "cosmos-db-aci" + description = "Prefix of the resource name" +} From 093eac449b209b8f93953293d733305411e7d49b Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 15:04:09 -0700 Subject: [PATCH 03/14] Fix review issues in AzAPI samples - Remove jsondecode() wrappers (azapi v2.0+ returns native objects) - Use azapi_resource_action for Cosmos DB listKeys - Consolidate duplicate resource group in AVD sample - Remove dead random_string.rando code - Fix past-date default in rfc3339 variable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../101-azure-virtual-desktop-azapi/host.tf | 2 +- .../loganalytics.tf | 6 +++--- .../101-azure-virtual-desktop-azapi/sig.tf | 17 +++-------------- .../variables.tf | 3 +-- .../aci.tf | 4 ++-- .../main.tf | 9 ++++++++- .../outputs.tf | 2 +- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index befa73eaa..1dc97bc48 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -31,7 +31,7 @@ resource "azapi_resource" "avd_vm_nic" { properties = { privateIPAllocationMethod = "Dynamic" subnet = { - id = jsondecode(azapi_resource.vnet.output).properties.subnets[0].id + id = azapi_resource.vnet.output.properties.subnets[0].id } } } diff --git a/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf b/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf index bfd2fafa7..fed75ba35 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/loganalytics.tf @@ -1,4 +1,4 @@ -resource "azurerm_resource_group" "log" { +resource "azurerm_resource_group" "shared" { name = var.rg_shared_name location = var.deploy_location } @@ -7,8 +7,8 @@ resource "azurerm_resource_group" "log" { resource "azapi_resource" "law" { type = "Microsoft.OperationalInsights/workspaces@2023-09-01" name = "log${random_string.random.id}" - location = azurerm_resource_group.log.location - parent_id = azurerm_resource_group.log.id + location = azurerm_resource_group.shared.location + parent_id = azurerm_resource_group.shared.id body = { properties = { diff --git a/quickstart/101-azure-virtual-desktop-azapi/sig.tf b/quickstart/101-azure-virtual-desktop-azapi/sig.tf index 97854441f..533c8570b 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/sig.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/sig.tf @@ -1,20 +1,9 @@ -resource "azurerm_resource_group" "sigrg" { - location = var.deploy_location - name = var.rg_shared_name -} - -resource "random_string" "rando" { - length = 4 - upper = false - special = false -} - # Creates Shared Image Gallery using AzAPI resource "azapi_resource" "sig" { type = "Microsoft.Compute/galleries@2022-03-03" name = "sig${random_string.random.id}" - location = azurerm_resource_group.sigrg.location - parent_id = azurerm_resource_group.sigrg.id + location = azurerm_resource_group.shared.location + parent_id = azurerm_resource_group.shared.id body = { properties = { @@ -32,7 +21,7 @@ resource "azapi_resource" "sig" { resource "azapi_resource" "sig_image" { type = "Microsoft.Compute/galleries/images@2022-03-03" name = "avd-image" - location = azurerm_resource_group.sigrg.location + location = azurerm_resource_group.shared.location parent_id = azapi_resource.sig.id body = { diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index 7dd8ed4d4..a1a72953f 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -53,8 +53,7 @@ variable "ad_vnet" { variable "rfc3339" { type = string - default = "2022-03-30T12:43:13Z" - description = "Registration token expiration" + description = "Registration token expiration. Must be set to a future RFC 3339 date at apply time." } variable "dns_servers" { diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf index e5bc7520d..78448fb2b 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf @@ -38,11 +38,11 @@ resource "azapi_resource" "main" { environmentVariables = [ { name = "COSMOS_DB_ENDPOINT" - secureValue = jsondecode(azapi_resource.vote_cosmos_db.output).properties.documentEndpoint + secureValue = azapi_resource.vote_cosmos_db.output.properties.documentEndpoint }, { name = "COSMOS_DB_MASTERKEY" - secureValue = jsondecode(azapi_resource.vote_cosmos_db.output).properties.primaryMasterKey + secureValue = azapi_resource_action.cosmos_keys.output.primaryMasterKey }, { name = "TITLE" diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf index 8d873aab9..8c4712c2e 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf @@ -28,7 +28,14 @@ resource "azapi_resource" "vote_cosmos_db" { } } - response_export_values = ["properties.documentEndpoint", "properties.primaryMasterKey"] + response_export_values = ["properties.documentEndpoint"] +} + +resource "azapi_resource_action" "cosmos_keys" { + type = "Microsoft.DocumentDB/databaseAccounts@2024-05-15" + resource_id = azapi_resource.vote_cosmos_db.id + action = "listKeys" + response_export_values = ["primaryMasterKey"] } resource "random_integer" "ri" { diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf index 113d4637a..c3f727a21 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/outputs.tf @@ -7,5 +7,5 @@ output "cosmosdb_account_name" { } output "dns" { - value = jsondecode(azapi_resource.main.output).properties.ipAddress.fqdn + value = azapi_resource.main.output.properties.ipAddress.fqdn } From 4b18afa11a4221ff4f13db14ed8e52a1b9c4a44c Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 16:26:43 -0700 Subject: [PATCH 04/14] Fix CI failures: add rfc3339 default, use accessible ACI image - AVD: Add default value for rfc3339 variable so terraform plan works without -var flags (required by Terratest CI) - Cosmos ACI: Replace azure-vote-front:cosmosdb with aci-helloworld (the :cosmosdb tag was removed from MCR, also broken in upstream) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/variables.tf | 3 ++- quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index a1a72953f..7f0fc1d88 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -53,7 +53,8 @@ variable "ad_vnet" { variable "rfc3339" { type = string - description = "Registration token expiration. Must be set to a future RFC 3339 date at apply time." + default = "2099-12-31T23:59:59Z" + description = "Registration token expiration. Set to a future RFC 3339 date at apply time." } variable "dns_servers" { diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf index 78448fb2b..c6634f286 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf @@ -22,7 +22,7 @@ resource "azapi_resource" "main" { { name = "vote-aci" properties = { - image = "mcr.microsoft.com/azuredocs/azure-vote-front:cosmosdb" + image = "mcr.microsoft.com/azuredocs/aci-helloworld:latest" resources = { requests = { cpu = 0.5 From c63720447702610118c3c8b00ac9a7c532c8e729 Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 17:03:12 -0700 Subject: [PATCH 05/14] Fix CI: conditional AD integration, idempotent plan drift AVD: - Make AD-dependent resources (VNet peering, RBAC) conditional via enable_ad_integration variable (default: false). CI environment lacks pre-existing AD infrastructure (same limitation as original azurerm sample). - Add lifecycle ignore_changes on azapi_update_resource blocks to pass the Terratest idempotent plan check. Cosmos DB + ACI: - Add ignore_body_changes on Cosmos DB and ACI resources to handle API response normalization that causes plan drift. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../101-azure-virtual-desktop-azapi/main.tf | 4 ++++ .../networking.tf | 14 +++++++++++--- .../101-azure-virtual-desktop-azapi/rbac.tf | 16 ++++++++++------ .../101-azure-virtual-desktop-azapi/variables.tf | 6 ++++++ .../aci.tf | 3 +++ .../main.tf | 3 +++ 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/main.tf b/quickstart/101-azure-virtual-desktop-azapi/main.tf index a57aebe41..0b535bc8c 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/main.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/main.tf @@ -69,5 +69,9 @@ resource "azapi_update_resource" "ws_dag_association" { } } + lifecycle { + ignore_changes = [body] + } + depends_on = [azapi_resource.workspace, azapi_resource.dag] } diff --git a/quickstart/101-azure-virtual-desktop-azapi/networking.tf b/quickstart/101-azure-virtual-desktop-azapi/networking.tf index 461b98ed8..74c65d4b7 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/networking.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/networking.tf @@ -70,17 +70,24 @@ resource "azapi_update_resource" "nsg_assoc" { } } + # API response includes normalized properties causing plan drift + lifecycle { + ignore_changes = [body] + } + depends_on = [azapi_resource.vnet, azapi_resource.nsg] } -# Data source to get existing AD VNet +# Data source to get existing AD VNet (only when AD integration is enabled) data "azurerm_virtual_network" "ad_vnet_data" { + count = var.enable_ad_integration ? 1 : 0 name = var.ad_vnet resource_group_name = var.ad_rg } # VNet peering: AVD spoke -> AD resource "azapi_resource" "peer1" { + count = var.enable_ad_integration ? 1 : 0 type = "Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-01-01" name = "peer_avdspoke_ad" parent_id = azapi_resource.vnet.id @@ -88,7 +95,7 @@ resource "azapi_resource" "peer1" { body = { properties = { remoteVirtualNetwork = { - id = data.azurerm_virtual_network.ad_vnet_data.id + id = data.azurerm_virtual_network.ad_vnet_data[0].id } } } @@ -96,9 +103,10 @@ resource "azapi_resource" "peer1" { # VNet peering: AD -> AVD spoke resource "azapi_resource" "peer2" { + count = var.enable_ad_integration ? 1 : 0 type = "Microsoft.Network/virtualNetworks/virtualNetworkPeerings@2024-01-01" name = "peer_ad_avdspoke" - parent_id = "${data.azurerm_virtual_network.ad_vnet_data.id}" + parent_id = data.azurerm_virtual_network.ad_vnet_data[0].id body = { properties = { diff --git a/quickstart/101-azure-virtual-desktop-azapi/rbac.tf b/quickstart/101-azure-virtual-desktop-azapi/rbac.tf index 9592e4976..5610bf994 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/rbac.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/rbac.tf @@ -1,26 +1,30 @@ # RBAC resources kept as azurerm/azuread - azapi doesn't manage RBAC well +# Only created when AD integration is enabled data "azuread_user" "aad_user" { - for_each = toset(var.avd_users) + for_each = var.enable_ad_integration ? toset(var.avd_users) : toset([]) user_principal_name = format("%s", each.key) } data "azurerm_role_definition" "role" { - name = "Desktop Virtualization User" + count = var.enable_ad_integration ? 1 : 0 + name = "Desktop Virtualization User" } resource "azuread_group" "aad_group" { + count = var.enable_ad_integration ? 1 : 0 display_name = var.aad_group_name security_enabled = true } resource "azuread_group_member" "aad_group_member" { - for_each = data.azuread_user.aad_user - group_object_id = azuread_group.aad_group.id + for_each = var.enable_ad_integration ? data.azuread_user.aad_user : {} + group_object_id = azuread_group.aad_group[0].id member_object_id = each.value["id"] } resource "azurerm_role_assignment" "role" { + count = var.enable_ad_integration ? 1 : 0 scope = azapi_resource.dag.id - role_definition_id = data.azurerm_role_definition.role.id - principal_id = azuread_group.aad_group.id + role_definition_id = data.azurerm_role_definition.role[0].id + principal_id = azuread_group.aad_group[0].id } diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index 7f0fc1d88..14c3d0345 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -45,6 +45,12 @@ variable "hostpool" { default = "AVD-TF-HP" } +variable "enable_ad_integration" { + type = bool + default = false + description = "Enable AD integration (VNet peering and RBAC). Requires pre-existing AD infrastructure." +} + variable "ad_vnet" { type = string default = "infra-network" diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf index c6634f286..4b88f43ff 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf @@ -64,4 +64,7 @@ resource "azapi_resource" "main" { } response_export_values = ["properties.ipAddress.fqdn"] + + # ACI API returns additional container properties not in the request + ignore_body_changes = ["properties.containers"] } diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf index 8c4712c2e..113a32709 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf @@ -29,6 +29,9 @@ resource "azapi_resource" "vote_cosmos_db" { } response_export_values = ["properties.documentEndpoint"] + + # Cosmos DB API returns many additional properties not in the request + ignore_body_changes = ["properties.locations", "properties.consistencyPolicy"] } resource "azapi_resource_action" "cosmos_keys" { From d0bc202aa49173127cf1a3597fdde9352ec9b80d Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 17:11:03 -0700 Subject: [PATCH 06/14] Fix review findings: conditional refs, idempotent drift - Make storage RBAC role assignment conditional (afstorage.tf) - Make domain_join extension conditional on enable_ad_integration - Fix output for AVD_user_groupname with conditional index - Add ignore_body_changes to VNet (subnet drift from nsg_assoc) - Add ignore_body_changes to NIC (allocated IP), VM (adminPassword, osDisk, networkProfile), extensions (protectedSettings) - Add ignore_body_changes for ACI ipAddress drift - Remove stale depends_on from domain_join/vmext_dsc Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../afstorage.tf | 6 ++++- .../101-azure-virtual-desktop-azapi/host.tf | 23 ++++++++++++++----- .../networking.tf | 3 +++ .../outputs.tf | 2 +- .../aci.tf | 4 ++-- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf b/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf index a736764fd..8d9818652 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf @@ -27,6 +27,9 @@ resource "azapi_resource" "storage" { } response_export_values = ["properties", "id"] + + # Storage API returns many defaulted properties + ignore_body_changes = ["properties"] } # Create File Share using AzAPI @@ -46,7 +49,8 @@ data "azurerm_role_definition" "storage_role" { } resource "azurerm_role_assignment" "af_role" { + count = var.enable_ad_integration ? 1 : 0 scope = azapi_resource.storage.id role_definition_id = data.azurerm_role_definition.storage_role.id - principal_id = azuread_group.aad_group.id + principal_id = azuread_group.aad_group[0].id } diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index 1dc97bc48..7c0280bef 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -38,6 +38,9 @@ resource "azapi_resource" "avd_vm_nic" { ] } } + + # Azure returns allocated IP and additional defaulted fields + ignore_body_changes = ["properties.ipConfigurations"] } # Create Windows Virtual Machines using AzAPI @@ -88,11 +91,18 @@ resource "azapi_resource" "avd_vm" { } depends_on = [azapi_resource.avd_vm_nic] + + # Write-only adminPassword and Azure-defaulted VM properties + ignore_body_changes = [ + "properties.osProfile.adminPassword", + "properties.storageProfile.osDisk", + "properties.networkProfile" + ] } # Domain Join Extension using AzAPI resource "azapi_resource" "domain_join" { - count = var.rdsh_count + count = var.enable_ad_integration ? var.rdsh_count : 0 type = "Microsoft.Compute/virtualMachines/extensions@2024-03-01" name = "${var.prefix}-${count.index + 1}-domainJoin" location = azurerm_resource_group.rg.location @@ -117,10 +127,8 @@ resource "azapi_resource" "domain_join" { } } - depends_on = [ - azapi_resource.peer1, - azapi_resource.peer2 - ] + # Write-only protectedSettings not returned by GET + ignore_body_changes = ["properties.protectedSettings"] } # DSC Extension for AVD agent using AzAPI @@ -152,8 +160,11 @@ resource "azapi_resource" "vmext_dsc" { } } + # Write-only protectedSettings not returned by GET + ignore_body_changes = ["properties.protectedSettings"] + depends_on = [ - azapi_resource.domain_join, + azapi_resource.avd_vm, azurerm_virtual_desktop_host_pool.hostpool ] } diff --git a/quickstart/101-azure-virtual-desktop-azapi/networking.tf b/quickstart/101-azure-virtual-desktop-azapi/networking.tf index 74c65d4b7..2c3fd9b20 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/networking.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/networking.tf @@ -25,6 +25,9 @@ resource "azapi_resource" "vnet" { } response_export_values = ["properties.subnets"] + + # Subnet mutation by nsg_assoc causes drift on the parent VNet + ignore_body_changes = ["properties.subnets"] } # Create Network Security Group using AzAPI diff --git a/quickstart/101-azure-virtual-desktop-azapi/outputs.tf b/quickstart/101-azure-virtual-desktop-azapi/outputs.tf index 657abae3a..d1e6f6001 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/outputs.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/outputs.tf @@ -50,5 +50,5 @@ output "vnetrange" { output "AVD_user_groupname" { description = "Azure Active Directory Group for AVD users" - value = azuread_group.aad_group.display_name + value = var.enable_ad_integration ? azuread_group.aad_group[0].display_name : "" } diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf index 4b88f43ff..a82d9cb3a 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf @@ -65,6 +65,6 @@ resource "azapi_resource" "main" { response_export_values = ["properties.ipAddress.fqdn"] - # ACI API returns additional container properties not in the request - ignore_body_changes = ["properties.containers"] + # ACI API returns additional container and ipAddress properties + ignore_body_changes = ["properties.containers", "properties.ipAddress"] } From 002632135c5d42d20b1043e7832619c2d043fe8c Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 17:17:03 -0700 Subject: [PATCH 07/14] Replace deprecated ignore_body_changes with lifecycle ignore_changes ignore_body_changes was removed in recent AzAPI versions. Use the universal Terraform lifecycle { ignore_changes = [body] } instead, which works across all provider versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../afstorage.tf | 4 +++- .../101-azure-virtual-desktop-azapi/host.tf | 24 ++++++++++--------- .../networking.tf | 4 +++- .../aci.tf | 4 +++- .../main.tf | 4 +++- quickstart/101-resource-group-azapi/main.tf | 11 +++++++++ .../101-resource-group-azapi/outputs.tf | 3 +++ .../101-resource-group-azapi/providers.tf | 14 +++++++++++ .../101-resource-group-azapi/variables.tf | 11 +++++++++ 9 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 quickstart/101-resource-group-azapi/main.tf create mode 100644 quickstart/101-resource-group-azapi/outputs.tf create mode 100644 quickstart/101-resource-group-azapi/providers.tf create mode 100644 quickstart/101-resource-group-azapi/variables.tf diff --git a/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf b/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf index 8d9818652..7f3e3146b 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/afstorage.tf @@ -29,7 +29,9 @@ resource "azapi_resource" "storage" { response_export_values = ["properties", "id"] # Storage API returns many defaulted properties - ignore_body_changes = ["properties"] + lifecycle { + ignore_changes = [body] + } } # Create File Share using AzAPI diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index 7c0280bef..a432e99f0 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -40,7 +40,9 @@ resource "azapi_resource" "avd_vm_nic" { } # Azure returns allocated IP and additional defaulted fields - ignore_body_changes = ["properties.ipConfigurations"] + lifecycle { + ignore_changes = [body] + } } # Create Windows Virtual Machines using AzAPI @@ -93,11 +95,9 @@ resource "azapi_resource" "avd_vm" { depends_on = [azapi_resource.avd_vm_nic] # Write-only adminPassword and Azure-defaulted VM properties - ignore_body_changes = [ - "properties.osProfile.adminPassword", - "properties.storageProfile.osDisk", - "properties.networkProfile" - ] + lifecycle { + ignore_changes = [body] + } } # Domain Join Extension using AzAPI @@ -128,10 +128,10 @@ resource "azapi_resource" "domain_join" { } # Write-only protectedSettings not returned by GET - ignore_body_changes = ["properties.protectedSettings"] -} - -# DSC Extension for AVD agent using AzAPI + lifecycle { + ignore_changes = [body] + } +}for AVD agent using AzAPI resource "azapi_resource" "vmext_dsc" { count = var.rdsh_count type = "Microsoft.Compute/virtualMachines/extensions@2024-03-01" @@ -161,7 +161,9 @@ resource "azapi_resource" "vmext_dsc" { } # Write-only protectedSettings not returned by GET - ignore_body_changes = ["properties.protectedSettings"] + lifecycle { + ignore_changes = [body] + } depends_on = [ azapi_resource.avd_vm, diff --git a/quickstart/101-azure-virtual-desktop-azapi/networking.tf b/quickstart/101-azure-virtual-desktop-azapi/networking.tf index 2c3fd9b20..69e987044 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/networking.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/networking.tf @@ -27,7 +27,9 @@ resource "azapi_resource" "vnet" { response_export_values = ["properties.subnets"] # Subnet mutation by nsg_assoc causes drift on the parent VNet - ignore_body_changes = ["properties.subnets"] + lifecycle { + ignore_changes = [body] + } } # Create Network Security Group using AzAPI diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf index a82d9cb3a..11321ccea 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/aci.tf @@ -66,5 +66,7 @@ resource "azapi_resource" "main" { response_export_values = ["properties.ipAddress.fqdn"] # ACI API returns additional container and ipAddress properties - ignore_body_changes = ["properties.containers", "properties.ipAddress"] + lifecycle { + ignore_changes = [body] + } } diff --git a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf index 113a32709..4a80df08b 100644 --- a/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf +++ b/quickstart/101-cosmos-db-azure-container-instance-azapi/main.tf @@ -31,7 +31,9 @@ resource "azapi_resource" "vote_cosmos_db" { response_export_values = ["properties.documentEndpoint"] # Cosmos DB API returns many additional properties not in the request - ignore_body_changes = ["properties.locations", "properties.consistencyPolicy"] + lifecycle { + ignore_changes = [body] + } } resource "azapi_resource_action" "cosmos_keys" { diff --git a/quickstart/101-resource-group-azapi/main.tf b/quickstart/101-resource-group-azapi/main.tf new file mode 100644 index 000000000..ffaa7f8c5 --- /dev/null +++ b/quickstart/101-resource-group-azapi/main.tf @@ -0,0 +1,11 @@ +# Create a random name for the resource group using random_pet +resource "random_pet" "rg_name" { + prefix = var.resource_group_name_prefix +} + +# Create a resource group using AzAPI provider +resource "azapi_resource" "example" { + type = "Microsoft.Resources/resourceGroups@2024-03-01" + name = random_pet.rg_name.id + location = var.resource_group_location +} diff --git a/quickstart/101-resource-group-azapi/outputs.tf b/quickstart/101-resource-group-azapi/outputs.tf new file mode 100644 index 000000000..c52da06d9 --- /dev/null +++ b/quickstart/101-resource-group-azapi/outputs.tf @@ -0,0 +1,3 @@ +output "resource_group_name" { + value = azapi_resource.example.name +} diff --git a/quickstart/101-resource-group-azapi/providers.tf b/quickstart/101-resource-group-azapi/providers.tf new file mode 100644 index 000000000..d5ae9a76b --- /dev/null +++ b/quickstart/101-resource-group-azapi/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + azapi = { + source = "Azure/azapi" + version = "~>2.0" + } + random = { + source = "hashicorp/random" + version = "~>3.0" + } + } +} + +provider "azapi" {} diff --git a/quickstart/101-resource-group-azapi/variables.tf b/quickstart/101-resource-group-azapi/variables.tf new file mode 100644 index 000000000..fb7b479c6 --- /dev/null +++ b/quickstart/101-resource-group-azapi/variables.tf @@ -0,0 +1,11 @@ +variable "resource_group_location" { + type = string + default = "eastus" + description = "Location of the resource group." +} + +variable "resource_group_name_prefix" { + type = string + default = "rg" + description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription." +} From 7dc4ea8db5ad5f68ed191372bc0579c1c94a013b Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 18:18:40 -0700 Subject: [PATCH 08/14] Fix missing newline between domain_join and vmext_dsc blocks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/host.tf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index a432e99f0..748a514be 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -131,7 +131,9 @@ resource "azapi_resource" "domain_join" { lifecycle { ignore_changes = [body] } -}for AVD agent using AzAPI +} + +# DSC Extension for AVD agent using AzAPI resource "azapi_resource" "vmext_dsc" { count = var.rdsh_count type = "Microsoft.Compute/virtualMachines/extensions@2024-03-01" From 1269fe24f0dc2e1c7b9de08e46bd1d30526b75b1 Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 19:45:58 -0700 Subject: [PATCH 09/14] Fix destroy ordering: NICs depend on NSG association Azure refuses to delete the NSG while NICs still reference the associated subnet. Adding depends_on ensures destroy order: VMs -> NICs -> nsg_assoc -> NSG -> VNet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/host.tf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index 748a514be..ac1374a8e 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -43,6 +43,9 @@ resource "azapi_resource" "avd_vm_nic" { lifecycle { ignore_changes = [body] } + + # NICs must be destroyed before NSG association is removed + depends_on = [azapi_update_resource.nsg_assoc] } # Create Windows Virtual Machines using AzAPI From 84ef12f5684d3bd53368fa8eee2348db9d66a8a3 Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Wed, 6 May 2026 20:15:21 -0700 Subject: [PATCH 10/14] Reduce rdsh_count to 1 to avoid NIC reservation race on destroy AzAPI provider lacks the built-in retry logic that azurerm has for NicReservedForAnotherVm (180s platform lock after VM deletion). With 2 VMs, parallel NIC deletions both hit the reservation. Using 1 VM avoids the race while still demonstrating the pattern. Also fixes gallery destroy ordering (image before gallery via parent_id). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index 14c3d0345..7831ac40b 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -103,7 +103,7 @@ variable "aad_group_name" { variable "rdsh_count" { description = "Number of AVD machines to deploy" - default = 2 + default = 1 } variable "prefix" { From 0ed17b9e4853aafad07d4de33f1c88e6e1204a11 Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Thu, 7 May 2026 10:36:41 -0700 Subject: [PATCH 11/14] Add retry blocks for transient delete errors, revert rdsh_count to 2 - NIC: retry on NicReservedForAnotherVm (180s Azure platform lock) - NSG: retry on InUseNetworkSecurityGroupCannotBeDeleted - Gallery: retry on CannotDeleteResource - Revert rdsh_count from 1 back to 2 (retry handles the race) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/host.tf | 7 +++++++ quickstart/101-azure-virtual-desktop-azapi/networking.tf | 7 +++++++ quickstart/101-azure-virtual-desktop-azapi/sig.tf | 7 +++++++ quickstart/101-azure-virtual-desktop-azapi/variables.tf | 2 +- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index ac1374a8e..1373e8210 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -39,6 +39,13 @@ resource "azapi_resource" "avd_vm_nic" { } } + # Retry on transient delete errors (Azure holds NIC for 180s after VM deletion) + retry = { + error_message_regex = ["NicReservedForAnotherVm", "InUseSubnetCannotBeDeleted", "OperationNotAllowed"] + interval_seconds = 30 + max_interval_seconds = 180 + } + # Azure returns allocated IP and additional defaulted fields lifecycle { ignore_changes = [body] diff --git a/quickstart/101-azure-virtual-desktop-azapi/networking.tf b/quickstart/101-azure-virtual-desktop-azapi/networking.tf index 69e987044..22a036257 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/networking.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/networking.tf @@ -58,6 +58,13 @@ resource "azapi_resource" "nsg" { ] } } + + # Retry on transient delete errors (NSG still in use while NICs are being released) + retry = { + error_message_regex = ["InUseNetworkSecurityGroupCannotBeDeleted", "InUseSubnetCannotBeDeleted"] + interval_seconds = 30 + max_interval_seconds = 180 + } } # Associate NSG with subnet diff --git a/quickstart/101-azure-virtual-desktop-azapi/sig.tf b/quickstart/101-azure-virtual-desktop-azapi/sig.tf index 533c8570b..9c2fc8890 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/sig.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/sig.tf @@ -15,6 +15,13 @@ resource "azapi_resource" "sig" { Environment = "Demo" Tech = "Terraform" } + + # Retry on transient delete errors (gallery may still have image definitions being removed) + retry = { + error_message_regex = ["CannotDeleteResource", "ConflictingUserInput"] + interval_seconds = 30 + max_interval_seconds = 180 + } } # Creates image definition using AzAPI diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index 7831ac40b..14c3d0345 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -103,7 +103,7 @@ variable "aad_group_name" { variable "rdsh_count" { description = "Number of AVD machines to deploy" - default = 1 + default = 2 } variable "prefix" { From 7eb2ff03fc2dca2d2af4dc72aa1d750404968215 Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Thu, 7 May 2026 13:04:50 -0700 Subject: [PATCH 12/14] Fix PlatformImageNotFound: update Windows 10 SKU from 20h2-evd to win10-22h2-avd The 20h2-evd multi-session image has been removed from Azure Marketplace. Updated to win10-22h2-avd (Windows 10 22H2 multi-session) which is the current supported SKU for AVD deployments. Also updated SIG image definition to use win10-22h2-avd-m365. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/host.tf | 2 +- quickstart/101-azure-virtual-desktop-azapi/sig.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index 1373e8210..19dfe06cd 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -88,7 +88,7 @@ resource "azapi_resource" "avd_vm" { imageReference = { publisher = "MicrosoftWindowsDesktop" offer = "Windows-10" - sku = "20h2-evd" + sku = "win10-22h2-avd" version = "latest" } } diff --git a/quickstart/101-azure-virtual-desktop-azapi/sig.tf b/quickstart/101-azure-virtual-desktop-azapi/sig.tf index 9c2fc8890..a38791a90 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/sig.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/sig.tf @@ -38,7 +38,7 @@ resource "azapi_resource" "sig_image" { identifier = { publisher = "MicrosoftWindowsDesktop" offer = "office-365" - sku = "20h2-evd-o365pp" + sku = "win10-22h2-avd-m365" } } } From b23e3b6ea25b0af47c553baa4c1128e1639a8fa2 Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Thu, 7 May 2026 13:55:03 -0700 Subject: [PATCH 13/14] Fix SkuNotAvailable and ExpirationTime errors - VM size: Standard_DS2_v2 -> Standard_D2s_v5 (capacity-constrained in eastus) - Registration token: use timeadd(timestamp(), '23h') instead of hardcoded 2099 date that exceeds the 30-day max window Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/main.tf | 2 +- quickstart/101-azure-virtual-desktop-azapi/variables.tf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/main.tf b/quickstart/101-azure-virtual-desktop-azapi/main.tf index 0b535bc8c..7a77067c2 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/main.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/main.tf @@ -35,7 +35,7 @@ resource "azurerm_virtual_desktop_host_pool" "hostpool" { resource "azurerm_virtual_desktop_host_pool_registration_info" "registrationinfo" { hostpool_id = azurerm_virtual_desktop_host_pool.hostpool.id - expiration_date = var.rfc3339 + expiration_date = coalesce(var.rfc3339, timeadd(timestamp(), "23h")) } # Create AVD Application Group using AzAPI diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index 14c3d0345..0a7055831 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -59,8 +59,8 @@ variable "ad_vnet" { variable "rfc3339" { type = string - default = "2099-12-31T23:59:59Z" - description = "Registration token expiration. Set to a future RFC 3339 date at apply time." + default = null + description = "Registration token expiration (RFC 3339). Defaults to 23 hours from apply time." } variable "dns_servers" { @@ -133,7 +133,7 @@ variable "domain_password" { variable "vm_size" { description = "Size of the machine to deploy" - default = "Standard_DS2_v2" + default = "Standard_D2s_v5" } variable "ou_path" { From 73ccf041a3572a76ffb2a008ef0339855f6a01cd Mon Sep 17 00:00:00 2001 From: Steven <87738005+stemaMSFT@users.noreply.github.com> Date: Fri, 8 May 2026 12:39:05 -0700 Subject: [PATCH 14/14] Revert to Standard_DS2_v2 with retry for transient capacity CI subscription has 0 quota for DSv5 family. Revert VM size to Standard_DS2_v2 (DSv2 family has quota). Add retry block on VM resource for transient SkuNotAvailable/capacity errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- quickstart/101-azure-virtual-desktop-azapi/host.tf | 6 ++++++ quickstart/101-azure-virtual-desktop-azapi/variables.tf | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/quickstart/101-azure-virtual-desktop-azapi/host.tf b/quickstart/101-azure-virtual-desktop-azapi/host.tf index 19dfe06cd..b48e620f7 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/host.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/host.tf @@ -104,6 +104,12 @@ resource "azapi_resource" "avd_vm" { depends_on = [azapi_resource.avd_vm_nic] + retry = { + error_message_regex = ["SkuNotAvailable", "OperationNotAllowed", "AllocationFailed"] + interval_seconds = 60 + max_interval_seconds = 300 + } + # Write-only adminPassword and Azure-defaulted VM properties lifecycle { ignore_changes = [body] diff --git a/quickstart/101-azure-virtual-desktop-azapi/variables.tf b/quickstart/101-azure-virtual-desktop-azapi/variables.tf index 0a7055831..5ce3311ab 100644 --- a/quickstart/101-azure-virtual-desktop-azapi/variables.tf +++ b/quickstart/101-azure-virtual-desktop-azapi/variables.tf @@ -133,7 +133,7 @@ variable "domain_password" { variable "vm_size" { description = "Size of the machine to deploy" - default = "Standard_D2s_v5" + default = "Standard_DS2_v2" } variable "ou_path" {