Site Loader

Introduction

This article is part one of a serie posts about WVD disk management. in this first part I will describe how to create and connect a new disk (based on a snapshot) to a new Azure VM based on the existing sessionhost configuration. This will save a lot of extra parameters like VMsize, network settings and type. After the VM is started you will get the information how to connect to the VM by RDP (3389) with specific credentials specially created for this VM.

At the end we have all the underlaying parts together.

  1. Create WVD image version based on existing config with PowerShell – Part 1
  2. Save WVD image with Sysprep as Image Gallery version – Part 2
  3. Create WVD Sessionhosts based on Shared Image Gallery version with ARM – Part 3
  4. WVD housekeeping, removing all unused sessionhosts, disks and images – Part 4 (coming soon)

Table of contents

  1. WVD in a nutshell
  2. Requirements
  3. Used Azure components
  4. Finding hostpool
  5. Sessionshosts
  6. Disk configuration
  7. Create VM
  8. Add your public IP to the NSG
  9. Generate credentials
  10. Results

WVD in a nutshell

Windows Virtual Desktop is a desktop and app virtualization service that runs on the cloud. For a full description click here
In the basic WVD exists on three big parts: clients, WVD and the Azure VM’s & Services. As the picture below says only WVD is Microsoft managed. For that part you can use ARM templates and you are out of control at that part.

WVD Architecture overview

The real fun starts at the dynamic environment like clients and VM’s. In this article we will focus how to deal with disk- and image management automatically.
Since WVD has no image provisioning like Citrix, disk management goes a bit different. Before knowing how to automate things it is good to know which elements we need and how the tasks need to be done when an image needs to be updated.

Requirements

PowerShell Modules

Microsoft has enrolled a new PowerShell module for Windows Virtual Desktop. For executing commands in this article you need this module. How to setup this module please check https://docs.microsoft.com/en-us/azure/virtual-desktop/powershell-module

We also need the az.network and az.compute PowerShell Modules

Snapshots

After you finished your job you need to sysprep (generalize) the disk before you can attach it to a sessionhost. You can run the sysprep command up to 8 times, after that you have to recreate a new Windows Image. To avoid that we creating a non sysprep version first (also call Before Snapshot) and create a disk from that one.
More info about Sysprep: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/sysprep–system-preparation–overview

Beforce executing the script make sure you have a before sysprep snapshot from a disk. You will need that snapshot for creating a new disk.

Used Azure components

  • Windows Virtual Desktop Hostpool
  • Virtual Machines (sessions hosts in a hostpool)
  • Disks & Snapshosts
  • Network
  • Shared Image Gallery

Finding hostpool

In this case I assume you already have a WVD environment with a hostpool, and sessionhosts. I will talk about WVD environment deployment later.

First we need to now in which hostpool you want to create a new disk. So the script will ask you for that. After all actually these are the only variables you really need :). Since the hostpool is the main WVD part we know every other component.
I’ve added an extra variable $publicIp. I will talk about this later in this topic. I’m also importing the needed modules.

param(
    [parameter(mandatory = $true)][string]$hostpoolName,
    [parameter(mandatory = $true)][string]$snapshotName,
    [parameter(mandatory = $true)][string]$localPublicIp
)

import-module az.desktopvirtualization
import-module az.network
import-module az.compute

$hostpool = Get-AzWvdHostPool | ? { $_.Name -eq $hostpoolname }
# Snapshot values 
$snapshot = get-azsnapshot -SnapshotName $snapshotname
$resourceGroup = $snapshot.ResourceGroupName
$location = $snapshot.Location

Sessionshosts

Now we have our hostpool and snapshot loaded, first lets determine which sessionshosts are in the hostpool. Before running the Get-AzWvdSessionHost command you need to know the resouregroupname where the hostpool is in. Let’s check the $hostpool variable from where we extract the resourcegroup.

$hostpoolResourceGroup = ($hostpool).id.split("/")[4]
$sessionHosts = Get-AzWvdSessionHost -ResourceGroupName $hostpoolResourceGroup -HostPoolName $hostpool.name

After loading the sessionshosts into a variable, let’s go futher gathering the VM configuration like hardware and networking. We selecting the first VM only and extracting all the info we need.

$sessionHostName = ($sessionHosts.Name.Split("/")[-1]).Split(".")[0]
$currentVmInfo = Get-AzVM -name $sessionHostName
$virtualMachineSize = $currentVmInfo.hardwareprofile.vmsize
$virtualNetworkSubnet = (Get-AzNetworkInterface -ResourceId $currentVmInfo.NetworkProfile.NetworkInterfaces.id).IpConfigurations.subnet.id
$NSG = Get-AzNetworkSecurityGroup | ? { $_.subnets.id -eq $virtualNetworkSubnet }
$virtualMachineResourceGroup = $currentVmInfo.ResourceGroupName

Disk configuration

When creating a new disk you need to setup a disk configuration. After then you can create a new disk if it not exists already.

$diskConfig = New-AzDiskConfig -SkuName "Premium_LRS" -Location $location -CreateOption Copy -SourceResourceId $snapshot.Id
$diskname = ('disk_' + $snapshot.name)
$disk = Get-azdisk -diskname $diskname
if ($disk) {
     Write-Output "Disk $diskname exists in resourcegroup $resourceGroupname"
    }
else {
New-AzDisk -Disk $diskConfig -ResourceGroupName $resourceGroupName -DiskName $diskName
#Test if disk is created
$disk = Get-azdisk -diskname $diskname
if ($disk) {
  Write-Output "Disk $diskname created succesful in resourcegroup $resourceGroupname"
    }
}

Create VM

Ok, we’ve stored every detail we need into variables. Let’s start creating a new VM with a new disk based on a snapshot.

$VirtualMachineName = ('vm' + $snapshot.name)
$VirtualMachine = New-AzVMConfig -VMName $VirtualMachineName -VMSize $virtualMachineSize
# Use the Managed Disk Resource Id to attach it to the virtual machine. Please change the OS type to linux if OS disk has linux OS
$VirtualMachine = Set-AzVMOSDisk -VM $VirtualMachine -ManagedDiskId $disk.Id -CreateOption Attach -Windows
# Create a public IP for the VM
$publicIp = New-AzPublicIpAddress -Name ($VirtualMachineName.ToLower() + '_ip') -ResourceGroupName $virtualMachineRg  -Location $snapshot.Location -AllocationMethod Dynamic -Force
# Create NIC in the first subnet of the virtual network
$nic = New-AzNetworkInterface -Name ($VirtualMachineName.ToLower() + '_nic') -ResourceGroupName $resourceGroupName -Location $snapshot.Location -SubnetId $virtualNetworkSubnet -PublicIpAddressId $publicIp.Id -Force
$VirtualMachine = Add-AzVMNetworkInterface -VM $VirtualMachine -Id $nic.Id

#Create the virtual machine with Managed Disk
$newVm = New-AzVM -VM $VirtualMachine -ResourceGroupName $resourceGroupName -Location $snapshot.Location

Add your public IP to the NSG

In the previous steps we loaded the Azure NSG into a variable. A NSG is the Network Security Group and is the firewall where the VM is behind. In the NSG you can add/remove network rules. Because you want to connect to the recently created VM with RDP (port 3389) you need to add an extra firewall rule which allows you to connect. Now the $publicIp variable is needed.
For adding a firewall rule I wrote a function first.

function add-firewallRule($NSG, $localPublicIp, $port) {
    # Pick random number for setting priority. It will exclude current priorities.
    $InputRange = 100..200
    $Exclude = ($NSG | Get-AzNetworkSecurityRuleConfig | select Priority).priority
    $RandomRange = $InputRange | Where-Object { $Exclude -notcontains $_ }
    $priority = Get-Random -InputObject $RandomRange
    $nsgParameters = @{
        Name                     = "Allow-$port-Inbound-$localPublicIp"
        Description              = "Allow port $port from local ip address $localPublicIp"
        Access                   = 'Allow'
        Protocol                 = "Tcp" 
        Direction                = "Inbound" 
        Priority                 = $priority 
        SourceAddressPrefix      = $localPublicIp 
        SourcePortRange          = "*"
        DestinationAddressPrefix = "*" 
        DestinationPortRange     = $port
    }
    $NSG | Add-AzNetworkSecurityRuleConfig @NSGParameters  | Set-AzNetworkSecurityGroup 
}

#Adding the role
add-firewallRule -NSG $NSG -localPublicIp $localPublicIp -port 3389

When the VM is created we create an username and password in the VM by installing the VMAccessAgent extention.
https://docs.microsoft.com/en-us/troubleshoot/azure/virtual-machines/support-agent-extensions

Generate credentials

First we need to create a random username and password. I also created a function for that as well.

function create-randomString($type) {
    function Get-RandomCharacters($length, $characters) {
        $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length }
        $private:ofs = ""
        return [String]$characters[$random]
    }
    if ($type -eq 'username') {
        $username = Get-RandomCharacters -length 8 -characters 'abcdefghiklmnoprstuvwxyz'
        return $username
    }
    if ($type -eq 'password') {
        $password = Get-RandomCharacters -length 5 -characters 'abcdefghiklmnoprstuvwxyz'
        $password += Get-RandomCharacters -length 1 -characters 'ABCDEFGHKLMNOPRSTUVWXYZ'
        $password += Get-RandomCharacters -length 1 -characters '1234567890'
        $password += Get-RandomCharacters -length 1 -characters '!$%&/()=?}][{@#*+'
        return $password
    }
}

$userName = create-randomString -type 'username'
$password = create-randomString -type 'password'
# Convert to SecureString
[securestring]$secStringPassword = ConvertTo-SecureString $password -AsPlainText -Force
[pscredential]$creds = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword)
# For reset username/password
Set-AzVMAccessExtension -ResourceGroupName $resourceGroupName -Location $snapshot.Location -VMName $VirtualMachineName -Credential $creds -typeHandlerVersion "2.0" -Name VMAccessAgent

Results

At last we combining everything we know together to write the content to our screen.

$publicIp = (Get-AzPublicIpAddress | where { $_.name -match $VirtualMachineName }).IpAddress
$details = "VM $virtualmachinename created succesful"
$bodyValues = [Ordered]@{
    details                = $details
    hostPool               = $hostpoolName
    virtualMachineName     = $VirtualMachineName
    resourceGroupName      = $resourceGroupName
    virtualMachinePublicIp = $publicIp
    username               = $userName
    password               = $password
    virtualMachineDisk     = $diskname
    engineersIp            = $localPublicIp
}
Write-Host $bodyValues

At the end, you will need the build in some checks like if a disk and VM is really created or does a hostpool has some sessionhosts in the first place. In production of course we have lots of checks but for now i think this is a great step from where to start.

We also created a mailflow behind the script so the executor gets notified when a VM is read. We also using Azure Functions to run scripts like this after requesting the weburl.

The whole script from this article can be found at my Github Windows Virtual Desktop repository.
https://github.com/srozemuller/Windows-Virtual-Destkop

In the next episode I will describe how to finish the disk with an automated sysprep and moving the disk as an version into the Azure Shared Image Gallery.

Post Author: Sander Rozemuller

One Reply to “Windows Virtual Desktop Image Management Automated – Part 1 – Create WVD image version based on existing config with PowerShell”

Leave a Reply

Your email address will not be published. Required fields are marked *