Using GitHub and Terraform to deploy Azure resources - Part 8

Table Of Contents

Intro

In this part of the series, I am creating the AVD session hosts so my users can start using the new Azure environment. A session host is a virtual machine that users can log in to and use their corporate software while having low latency to other resources placed in Azure.

The steps I will go through in this post are.

  • Deploy a virtual machine
  • Install the AVD components on the virtual machine. This deployment is a DSC resource that includes the AVD agent
  • Join the virtual machine to Azure Active Directory
  • FSLogix configurations on the session hosts
  • Deployment and verification

Deploy a virtual machine

First, I will deploy a standard Windows virtual machine in Azure. I will use a Windows 11 multi-user operating system for this machine. Microsoft maintains this image, including Office 365 applications like Word, Excel, and Teams.

Before deploying the machine, I need to gather information from my environment. The machine needs to be in a subnet on a virtual network. I am using the data modules to get this information to do the lookups.

I first look up the virtual network, and then the subnet. I can then use the information to create a new network interface that I will attach to the virtual machine.

I also need to look up the password I will use for the local administrator on the virtual machine. This password is in an Azure key vault, so again I will use a data module to look up this information.

With this information, I can use the azurerm_windows_virtual_machine resource to deploy my new virtual machine.

I have added the code below to the sessionhost.tf file under the “rg-avd-cloudninja-001” folder.

data "azurerm_virtual_network" "AVD-vNet" {
  name                = var.avd_vnet
  resource_group_name = var.avd_vnet_resource_group
}

data "azurerm_subnet" "subnets" {
  name                  = var.avd_hostpool_subnet
  virtual_network_name  = data.azurerm_virtual_network.AVD-vNet.name
  resource_group_name   = data.azurerm_virtual_network.AVD-vNet.resource_group_name
 
}

resource "azurerm_network_interface" "main" {
  count               = var.NumberOfSessionHosts
  name                = "nic-${var.vm_prefix}-${format("%02d",count.index+1)}"
  location            = var.avd_Location
  resource_group_name = azurerm_resource_group.resourcegroup.name

  ip_configuration {
    name                          = "ipconfig"
    subnet_id                     = data.azurerm_subnet.subnets.id
    private_ip_address_allocation = "Dynamic"
  }
}

data "azurerm_key_vault" "kv-cloudninja-avd-002" {
  name                = "kv-cloudninja-avd-002"
  resource_group_name = "rg-keyvault-001"
}

data "azurerm_key_vault_secret" "avd-localadmin" {
  name         = "avd-localadmin"
  key_vault_id = data.azurerm_key_vault.kv-cloudninja-avd-002.id
}

resource "azurerm_windows_virtual_machine" "main" {
  count                 = var.NumberOfSessionHosts
  name                  = "vm-${var.vm_prefix}-${format("%02d",count.index+1)}"
  location            = var.avd_Location
  resource_group_name = azurerm_resource_group.resourcegroup.name
  network_interface_ids = [element(azurerm_network_interface.main.*.id, count.index)]
  size                  = "Standard_D2s_v3"
  license_type          = "Windows_Client"
  admin_username        = "localadmin"
  admin_password        = data.azurerm_key_vault_secret.avd-localadmin.value

  additional_capabilities {
  }
  identity {
    type = "SystemAssigned"
  }
  source_image_reference {
    offer     = "office-365"
    publisher = "microsoftwindowsdesktop"
    sku       = "win11-21h2-avd-m365"
    version   = "latest"
  }
  os_disk {
    name              = "vm-${var.vm_prefix}-${format("%02d",count.index+1)}-osdisk"
    caching              = "ReadWrite"
    storage_account_type = "StandardSSD_LRS"
  }  
}

Looking at the code above, you might have noticed that I am using a count setting on the resources I create. Using count ensures that I can deploy any number of virtual machines using the same code. Doing the deployment this way also means I can scale the solution up and down as needed.

To control these settings, I have added the code below to the variables.tf file under the “rg-avd-cloudninja-001” folder.

variable "NumberOfSessionHosts" {
    type = number
    default = 2
}

variable "vm_prefix" {
    type = string
    default = "avd-h1"
}

variable "avd_vnet" {
    type = string
    default = "vnet-avd-001"
}
variable "avd_vnet_resource_group" {
    type = string
    default = "rg-avd-network-001"
}
variable "avd_hostpool_subnet" {
    type = string
    default = "snet-avd-hostpool-001"
}

With the code above, I can now deploy the virtual machine.

Install the AVD components on the virtual machine

Now that I have the virtual machine code completed, I can deploy the AVD components to it. Since I want everything deployed immediately, I include more code in the same deployment file as the virtual machine. The DSC resource deployment uses a virtual machine extension deployment resource.

Below is the code I have added to the sessionhost.tf file under the “rg-avd-cloudninja-001” folder.

resource "azurerm_virtual_machine_extension" "dsc" {
  count = var.NumberOfSessionHosts
  name                 = "AddToAVD"
  virtual_machine_id   = element(azurerm_windows_virtual_machine.main.*.id, count.index)
  publisher            = "Microsoft.Powershell"
  type                 = "DSC"
  type_handler_version = "2.73"    
  auto_upgrade_minor_version = true

  settings           = <<SETTINGS
            {
                "modulesUrl": "$var.avd_agent_location",
                "configurationFunction": "Configuration.ps1\\AddSessionHost",            
                "properties": {
                    "hostPoolName": "${azurerm_virtual_desktop_host_pool.hostpool.name}",
                    "aadJoin": true,
                    "UseAgentDownloadEndpoint": true,
                    "aadJoinPreview": false,
                    "mdmId": "",
                    "sessionHostConfigurationLastUpdateTime": "",
                    "registrationInfoToken" : "${azurerm_virtual_desktop_host_pool_registration_info.registrationkey.token}" 
                }
            }
            SETTINGS  
  
    depends_on = [
        azurerm_windows_virtual_machine.main
    ]
}

As the code above shows, I am deploying DSC resources. I am fetching the resource from a URL site and applying it to the virtual machine. I am also selecting to use Azure AD for the AVD configuration for this virtual machine. I do need to have the registration token for the AVD host pool. This registration token is from the host pool we did earlier in this blog series.

Microsoft hosts the URL used for the DSC resource, and I have added the URL to my variables.tf file as shown below.

variable "avd_agent_location" {
    type = string 
    default = "https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_06-15-2022.zip"    
}

Join the virtual machine to Azure Active Directory

The last piece to the AVD session host puzzle is to join the virtual machine to Azure Active Directory. Azure AD join is a straightforward process. I only have to deploy another virtual machine extension to my VM. This extension is called AADLoginForWindows and will ensure the join to Azure AD.

Below is the code I have added to the sessionhost.tf file under the “rg-avd-cloudninja-001” folder.

resource "azurerm_virtual_machine_extension" "AADLoginForWindows" {
    count = var.NumberOfSessionHosts
    name                              = "AADLoginForWindows"
    virtual_machine_id   = element(azurerm_windows_virtual_machine.main.*.id, count.index)
    publisher                         = "Microsoft.Azure.ActiveDirectory"
    type                              = "AADLoginForWindows"
    type_handler_version              = "1.0"
    auto_upgrade_minor_version        = true
    depends_on = [
        azurerm_virtual_machine_extension.dsc
    ]
}

FSLogix configurations on the session hosts

For the FSLogix profiles to work, I need to create two registry settings on the virtual machine. Now it would be great if I could use Intune to make this work, but since I want my machines to be ready as fast as possible, this solution won’t be good enough for me.

The way I solved it was to run a set of PowerShell commands on the virtual machine using the Azure CLI. Running the command is straightforward, but remember that \ is an escape character, so two are needed to write one.

Below is the code I have added to the sessionhost.tf file under the “rg-avd-cloudninja-001” folder.

resource "null_resource" "FSLogix" {
  count = var.NumberOfSessionHosts
  provisioner "local-exec" {
    command = "az vm run-command invoke --command-id RunPowerShellScript --name ${element(azurerm_windows_virtual_machine.main.*.name, count.index)} -g ${azurerm_resource_group.resourcegroup.name} --scripts 'New-ItemProperty -Path HKLM:\\SOFTWARE\\FSLogix\\Profiles -Name VHDLocations -Value \\\\cloudninjafsl11072022.file.core.windows.net\\avdprofiles -PropertyType MultiString;New-ItemProperty -Path HKLM:\\SOFTWARE\\FSLogix\\Profiles -Name Enabled -Value 1 -PropertyType DWORD;New-ItemProperty -Path HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa\\Kerberos\\Parameters -Name CloudKerberosTicketRetrievalEnabled -Value 1 -PropertyType DWORD;New-Item -Path HKLM:\\Software\\Policies\\Microsoft\\ -Name AzureADAccount;New-ItemProperty -Path HKLM:\\Software\\Policies\\Microsoft\\AzureADAccount  -Name LoadCredKeyFromProfile -Value 1 -PropertyType DWORD;Restart-Computer'"
    interpreter = ["PowerShell", "-Command"]
  }
  depends_on = [
       azurerm_virtual_machine_extension.AADLoginForWindows
    ]
}

The code above uses a “null_resource.” Using the local-exec provider means the code runs on the machine executing the Terraform deployment. The resource performs the following actions in PowerShell on the AVD session host.

New-ItemProperty -Path HKLM:\SOFTWARE\FSLogix\Profiles -Name VHDLocations -Value \\cloudninjafsl11072022.file.core.windows.net\avdprofiles -PropertyType MultiString
New-ItemProperty -Path HKLM:\SOFTWARE\FSLogix\Profiles -Name Enabled -Value 1 -PropertyType DWORD
New-ItemProperty -Path HKLM:\SYSTEM\\CurrentControlSet\Control\Lsa\Kerberos\Parameters -Name CloudKerberosTicketRetrievalEnabled -Value 1 -PropertyType DWORD
New-Item -Path HKLM\Software\Policies\Microsoft\ -Name AzureADAccount
New-ItemProperty -Path HKLM:\Software\Policies\Microsoft\AzureADAccount  -Name LoadCredKeyFromProfile -Value 1 -PropertyType DWORD

Deployment and verification

With the above changes to my code, I can commit the changes to GitHub, and it will automatically kick off the GitHub Action that will deploy the session hosts to my environment. I can verify this in the Azure portal by going into the AVD Host pool and clicking on session hosts.

Below you can see what it looks like in my environment.

I can also log into the AVD using the web client (or the native client if installed) to verify that everything is working as expected. On the screenshot below, we can see the hostname of the AVD session host, my username, and FSLogix has successfully mounted my profile with all green lights.

Summary

I have successfully deployed my AVD environment and showed that I could log in to my new environment. There have been quite a few posts in this series, but I hope the content is helpful for you. I will probably pick this series up again in the near future, but this post concludes the steps I had planned out so far. Feel free to contact me if you have any ideas about additions or something you would like me to blog about.

Any feedback is welcome, so reach out on Twitter or LinkedIn, so I can fix any errors or optimize the code I am using.

Part 1: https://www.cloudninja.nu/post/2022/06/github-terraform-azure-part1/

Part 2: https://www.cloudninja.nu/post/2022/06/github-terraform-azure-part2/

Part 3: https://www.cloudninja.nu/post/2022/06/github-terraform-azure-part3/

Part 4: https://www.cloudninja.nu/post/2022/06/github-terraform-azure-part4/

Part 5: https://www.cloudninja.nu/post/2022/07/github-terraform-azure-part5/

Part 6: https://www.cloudninja.nu/post/2022/07/github-terraform-azure-part6/

Part 7: https://www.cloudninja.nu/post/2022/08/github-terraform-azure-part7/

Link for all the code in this post

I have put all the code used in this blog post on my GitHub repository, so you can download or fork the repository if you want to.

https://github.com/mracket/GitHub-Terraform

Comments