Configure Azure Virtual Desktop session hosts using Terraform

This article shows you how to build Session Hosts and deploy them to an AVD Host Pool with Terraform. This article assumes you've already deployed the Azure Virtual Desktop Infrastructure.

Article tested with the following Terraform and Terraform provider versions:

Learn more about using Terraform in Azure

In this article, you learn how to:

  • Use Terraform to create NIC for each session host
  • Use Terraform to create VM for session host
  • Join VM to domain
  • Register VM with Azure Virtual Desktop
  • Use variables file

1. Configure your environment

  • Azure subscription: If you don't have an Azure subscription, create a free account before you begin.

2. Implement the Terraform code

  1. Create a directory in which to test the sample Terraform code and make it the current directory.

  2. Create a file named providers.tf and insert the following code.

    terraform {
      required_providers {
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~>2.0"
        }
        azuread = {
          source = "hashicorp/azuread"
        }
      }
    }
    
    provider "azurerm" {
      features {}
    }
    

    Key points:

    • Use count to indicate how many resources will be created
    • References resources that were created when the infrastructure was built - such as azurerm_subnet.subnet.id and azurerm_virtual_desktop_host_pool.hostpool.name. If you changed the name of these resources from that section, you also need to update the references here.
  3. Create a file named main.tf and insert the following code:

    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
    }
    
    resource "azurerm_network_interface" "avd_vm_nic" {
      count               = var.rdsh_count
      name                = "${var.prefix}-${count.index + 1}-nic"
      resource_group_name = azurerm_resource_group.rg.name
      location            = azurerm_resource_group.rg.location
    
      ip_configuration {
        name                          = "nic${count.index + 1}_config"
        subnet_id                     = azurerm_subnet.subnet.id
        private_ip_address_allocation = "dynamic"
      }
    
      depends_on = [
        azurerm_resource_group.rg
      ]
    }
    
    resource "azurerm_windows_virtual_machine" "avd_vm" {
      count                 = var.rdsh_count
      name                  = "${var.prefix}-${count.index + 1}"
      resource_group_name   = azurerm_resource_group.rg.name
      location              = azurerm_resource_group.rg.location
      size                  = var.vm_size
      network_interface_ids = ["${azurerm_network_interface.avd_vm_nic.*.id[count.index]}"]
      provision_vm_agent    = true
      admin_username        = var.local_admin_username
      admin_password        = var.local_admin_password
    
      os_disk {
        name                 = "${lower(var.prefix)}-${count.index + 1}"
        caching              = "ReadWrite"
        storage_account_type = "Standard_LRS"
      }
    
      source_image_reference {
        publisher = "MicrosoftWindowsDesktop"
        offer     = "Windows-10"
        sku       = "20h2-evd"
        version   = "latest"
      }
    
      depends_on = [
        azurerm_resource_group.rg,
        azurerm_network_interface.avd_vm_nic
      ]
    }
    
    resource "azurerm_virtual_machine_extension" "domain_join" {
      count                      = var.rdsh_count
      name                       = "${var.prefix}-${count.index + 1}-domainJoin"
      virtual_machine_id         = azurerm_windows_virtual_machine.avd_vm.*.id[count.index]
      publisher                  = "Microsoft.Compute"
      type                       = "JsonADDomainExtension"
      type_handler_version       = "1.3"
      auto_upgrade_minor_version = true
    
      settings = <<SETTINGS
        {
          "Name": "${var.domain_name}",
          "OUPath": "${var.ou_path}",
          "User": "${var.domain_user_upn}@${var.domain_name}",
          "Restart": "true",
          "Options": "3"
        }
    SETTINGS
    
      protected_settings = <<PROTECTED_SETTINGS
        {
          "Password": "${var.domain_password}"
        }
    PROTECTED_SETTINGS
    
      lifecycle {
        ignore_changes = [settings, protected_settings]
      }
    
      depends_on = [
        azurerm_virtual_network_peering.peer1,
        azurerm_virtual_network_peering.peer2
      ]
    }
    
    resource "azurerm_virtual_machine_extension" "vmext_dsc" {
      count                      = var.rdsh_count
      name                       = "${var.prefix}${count.index + 1}-avd_dsc"
      virtual_machine_id         = azurerm_windows_virtual_machine.avd_vm.*.id[count.index]
      publisher                  = "Microsoft.Powershell"
      type                       = "DSC"
      type_handler_version       = "2.73"
      auto_upgrade_minor_version = true
    
      settings = <<-SETTINGS
        {
          "modulesUrl": "https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_09-08-2022.zip",
          "configurationFunction": "Configuration.ps1\\AddSessionHost",
          "properties": {
            "HostPoolName":"${azurerm_virtual_desktop_host_pool.hostpool.name}"
          }
        }
    SETTINGS
    
      protected_settings = <<PROTECTED_SETTINGS
      {
        "properties": {
          "registrationInfoToken": "${local.registration_token}"
        }
      }
    PROTECTED_SETTINGS
    
      depends_on = [
        azurerm_virtual_machine_extension.domain_join,
        azurerm_virtual_desktop_host_pool.hostpool
      ]
    }
    
  4. Create a file named variables.tf and insert the following code:

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 "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" # do not include domain name as this is appended
  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
}
  1. Create a file named output.tf and insert the following code:
output "location" {
  description = "The Azure region"
  value       = azurerm_resource_group.rg.location
}

output "session_host_count" {
  description = "The number of VMs created"
  value       = var.rdsh_count
}

output "dnsservers" {
  description = "Custom DNS configuration"
  value       = azurerm_virtual_network.vnet.dns_servers
}

output "vnetrange" {
  description = "Address range for deployment vnet"
  value       = azurerm_virtual_network.vnet.address_space
}
  1. Create a file named terraform.tfvars and insert the following code:

    # Customized the sample values below for your environment and either rename to terraform.tfvars or env.auto.tfvars
    
    deploy_location      = "west europe"
    rg_name              = "avd-resources-rg"
    prefix               = "avdtf"
    local_admin_username = "localadm"
    local_admin_password = "ChangeMe123$"
    vnet_range           = ["10.1.0.0/16"]
    subnet_range         = ["10.1.0.0/24"]
    dns_servers          = ["10.0.1.4", "168.63.129.16"]
    aad_group_name       = "AVDUsers"
    domain_name          = "infra.local"
    domain_user_upn      = "admin"     # do not include domain name as this is appended
    domain_password      = "ChangeMe123!"
    ad_vnet              = "infra-network"
    ad_rg                = "infra-rg"
    avd_users = [
      "avduser01@infra.local",
      "avduser01@infra.local"
    ]
    

3. Initialize Terraform

Run terraform init to initialize the Terraform deployment. This command downloads the Azure provider required to manage your Azure resources.

terraform init -upgrade

Key points:

  • The -upgrade parameter upgrades the necessary provider plugins to the newest version that complies with the configuration's version constraints.

4. Create a Terraform execution plan

Run terraform plan to create an execution plan.

terraform plan -out main.tfplan

Key points:

  • The terraform plan command creates an execution plan, but doesn't execute it. Instead, it determines what actions are necessary to create the configuration specified in your configuration files. This pattern allows you to verify whether the execution plan matches your expectations before making any changes to actual resources.
  • The optional -out parameter allows you to specify an output file for the plan. Using the -out parameter ensures that the plan you reviewed is exactly what is applied.

5. Apply a Terraform execution plan

Run terraform apply to apply the execution plan to your cloud infrastructure.

terraform apply main.tfplan

Key points:

  • The example terraform apply command assumes you previously ran terraform plan -out main.tfplan.
  • If you specified a different filename for the -out parameter, use that same filename in the call to terraform apply.
  • If you didn't use the -out parameter, call terraform apply without any parameters.

6. Verify the results

  1. On the Azure portal, Select Azure Virtual Desktop.
  2. Select Host pools and then the Name of the pool created resource.
  3. Select Session hosts and then verify the session host is listed.

7. Clean up resources

When you no longer need the resources created via Terraform, do the following steps:

  1. Run terraform plan and specify the destroy flag.

    terraform plan -destroy -out main.destroy.tfplan
    

    Key points:

    • The terraform plan command creates an execution plan, but doesn't execute it. Instead, it determines what actions are necessary to create the configuration specified in your configuration files. This pattern allows you to verify whether the execution plan matches your expectations before making any changes to actual resources.
    • The optional -out parameter allows you to specify an output file for the plan. Using the -out parameter ensures that the plan you reviewed is exactly what is applied.
  2. Run terraform apply to apply the execution plan.

    terraform apply main.destroy.tfplan
    

Troubleshoot Terraform on Azure

Troubleshoot common problems when using Terraform on Azure

Next steps