Windows Virtual Desktop Host Pool Automation Part 2

Windows Virtual Desktop Host Pool Automation Part 2

Hello everyone, it´s Patrick again!

I received a lot of great feedback for the first part of the Host Pool / Session Host automation article and a lot of questions reached me “how to get started with the domain join DSC and an automated way to shutdown and start the session hosts during off-peak hours”.

And exactly this will be topic in todays tutorial, so let´s directly jump into it!

Important side note:

Microsoft has announced today, 13.05.2020 that they are developing a solution for automatic scaling capabilities for Microsoft Teams. The link, with the announcements can be found here.

Solution 1: Virtual Machine Scale Sets with Domain Join DSC

To save you a lot of time in the deployment itself, I´ve created an ARM based template for you to deploy your Virtual Machine Scale Set directly from over there. We start logging into the Azure Portal by going to: https://portal.azure.com and logging in with our administrative credentials.

Now we´re going to the search bar and type templates, because the goal is to use a pre-defined ARM template and adjust the settings to our needs.

From within the templates pane, we click on “+Add” to add a new template. The template I provide you with can be used at any later time in your deployment phase.

Give your template a friendly name and description for later usage and click “OK”. The next you will see is a sample template provided by Azure, replace it with the following template:

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "location": {
            "type": "String"
        },
        "virtualMachineScaleSetName": {
            "type": "String"
        },
        "virtualMachineScaleSetRG": {
            "type": "String"
        },
        "singlePlacementGroup": {
            "type": "String"
        },
        "instanceSize": {
            "type": "String"
        },
        "instanceCount": {
            "type": "String"
        },
        "upgradeMode": {
            "type": "String"
        },
        "priority": {
            "type": "String"
        },
        "enableAcceleratedNetworking": {
            "type": "String"
        },
        "subnetId": {
            "type": "String"
        },
        "osDiskType": {
            "type": "String"
        },
        "virtualNetworkId": {
            "type": "String"
        },
        "virtualNetworkName": {
            "type": "String"
        },
        "networkInterfaceConfigurations": {
            "type": "Array"
        },
        "vmName": {
            "type": "String"
        },
        "scaleInPolicy": {
            "type": "Object"
        },
        "upgradePolicy": {
            "type": "String"
        },
        "adminUsername": {
            "type": "String"
        },
        "adminPassword": {
            "type": "SecureString"
        },
        "autoScaleDefault": {
            "type": "String"
        },
        "autoScaleMin": {
            "type": "String"
        },
        "autoScaleMax": {
            "type": "String"
        },
        "scaleOutCPUPercentageThreshold": {
            "type": "String"
        },
        "durationTimeWindow": {
            "type": "String"
        },
        "scaleOutInterval": {
            "type": "String"
        },
        "scaleInCPUPercentageThreshold": {
            "type": "String"
        },
        "scaleInInterval": {
            "type": "String"
        },
        "platformFaultDomainCount": {
            "type": "String"
        },
        "domainName": {
            "type": "String"
        },
        "OUDN": {
            "type": "String"
        },
        "domainAndUsername": {
            "type": "String",
            "metadata": {
                "description": "You must provide the username in the following convention NETBIOS BACKSLASH USERNAME "
            }
        },
        "domainJoinPassword": {
            "type": "SecureString"
        }
    },
    "variables": {
        "storageApiVersion": "2019-06-01",
        "namingInfix": "[toLower(substring(concat(parameters('virtualMachineScaleSetName'), uniqueString(resourceGroup().id)), 0, 9))]",
        "vmssId": "[resourceId('Microsoft.Compute/virtualMachineScaleSets', parameters('virtualMachineScaleSetName'))]",
        "autoScaleResourceName": "[concat(parameters('virtualMachineScaleSetName'), 'autoscale')]",
        "domainJoinOptions": 3
    },
    "resources": [
        {
            "type": "Microsoft.Insights/autoscaleSettings",
            "apiVersion": "2015-04-01",
            "name": "[variables('autoScaleResourceName')]",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[concat('Microsoft.Compute/virtualMachineScaleSets/', parameters('virtualMachineScaleSetName'))]"
            ],
            "properties": {
                "name": "[variables('autoScaleResourceName')]",
                "targetResourceUri": "[variables('vmssId')]",
                "enabled": true,
                "profiles": [
                    {
                        "name": "Profile1",
                        "capacity": {
                            "minimum": "[parameters('autoScaleMin')]",
                            "maximum": "[parameters('autoScaleMax')]",
                            "default": "[parameters('autoScaleDefault')]"
                        },
                        "rules": [
                            {
                                "metricTrigger": {
                                    "metricName": "Percentage CPU",
                                    "metricNamespace": "",
                                    "metricResourceUri": "[variables('vmssId')]",
                                    "timeGrain": "PT1M",
                                    "statistic": "Average",
                                    "timeWindow": "[concat('PT', parameters('durationTimeWindow'), 'M')]",
                                    "timeAggregation": "Average",
                                    "operator": "GreaterThan",
                                    "threshold": "[parameters('scaleOutCPUPercentageThreshold')]"
                                },
                                "scaleAction": {
                                    "direction": "Increase",
                                    "type": "ChangeCount",
                                    "value": "[parameters('scaleOutInterval')]",
                                    "cooldown": "PT1M"
                                }
                            },
                            {
                                "metricTrigger": {
                                    "metricName": "Percentage CPU",
                                    "metricNamespace": "",
                                    "metricResourceUri": "[variables('vmssId')]",
                                    "timeGrain": "PT1M",
                                    "statistic": "Average",
                                    "timeWindow": "PT5M",
                                    "timeAggregation": "Average",
                                    "operator": "LessThan",
                                    "threshold": "[parameters('scaleInCPUPercentageThreshold')]"
                                },
                                "scaleAction": {
                                    "direction": "Decrease",
                                    "type": "ChangeCount",
                                    "value": "[parameters('scaleInInterval')]",
                                    "cooldown": "PT1M"
                                }
                            }
                        ]
                    }
                ]
            }
        },
        {
            "type": "Microsoft.Compute/virtualMachineScaleSets",
            "apiVersion": "2019-07-01",
            "name": "[parameters('virtualMachineScaleSetName')]",
            "location": "[parameters('location')]",
            "dependsOn": [],
            "sku": {
                "name": "[parameters('instanceSize')]",
                "capacity": "[int(parameters('instanceCount'))]"
            },
            "properties": {
                "overprovision": "true",
                "upgradePolicy": {
                    "mode": "[parameters('upgradePolicy')]"
                },
                "singlePlacementGroup": "[parameters('singlePlacementGroup')]",
                "virtualMachineProfile": {
                    "storageProfile": {
                        "osDisk": {
                            "createOption": "fromImage",
                            "caching": "ReadWrite",
                            "managedDisk": {
                                "storageAccountType": "[parameters('osDiskType')]"
                            }
                        },
                        "imageReference": {
                            "publisher": "MicrosoftWindowsDesktop",
                            "offer": "Windows-10",
                            "sku": "19h2-evd",
                            "version": "latest"
                        }
                    },
                    "priority": "[parameters('priority')]",
                    "networkProfile": {
                        "copy": [
                            {
                                "name": "networkInterfaceConfigurations",
                                "count": "[length(parameters('networkInterfaceConfigurations'))]",
                                "input": {
                                    "name": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].name]",
                                    "properties": {
                                        "primary": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].primary]",
                                        "enableAcceleratedNetworking": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].enableAcceleratedNetworking]",
                                        "ipConfigurations": [
                                            {
                                                "name": "[concat(parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].name, '-defaultIpConfiguration')]",
                                                "properties": {
                                                    "subnet": {
                                                        "id": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].subnetId]"
                                                    },
                                                    "primary": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].primary]",
                                                    "applicationGatewayBackendAddressPools": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].applicationGatewayBackendAddressPools]",
                                                    "loadBalancerBackendAddressPools": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].loadBalancerBackendAddressPools]",
                                                    "loadBalancerInboundNatPools": "[parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].loadBalancerInboundNatPools]",
                                                    "publicIPAddressConfiguration": "[if( equals( parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].pipName, ''), json('null'), union(json(concat('{\"name\": \"', parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].pipName, '\"}'))\n                        ,json('{\"properties\": { \"idleTimeoutInMinutes\": 15}}')))]"
                                                }
                                            }
                                        ],
                                        "networkSecurityGroup": "[if( equals( parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].nsgId, ''), json('null'),json(concat('{\"id\": \"', parameters('networkInterfaceConfigurations')[copyIndex('networkInterfaceConfigurations')].nsgId, '\"}')))]"
                                    }
                                }
                            }
                        ]
                    },
                    "extensionProfile": {
                        "extensions": [
                            {
                                "name": "joindomain",
                                "properties": {
                                    "publisher": "Microsoft.Compute",
                                    "type": "JsonADDomainExtension",
                                    "typeHandlerVersion": "1.3",
                                    "settings": {
                                        "Name": "[parameters('domainName')]",
                                        "OUPath": "[parameters('OUDN')]",
                                        "User": "[parameters('domainAndUsername')]",
                                        "Restart": "true",
                                        "Options": "[variables('domainJoinOptions')]"
                                    },
                                    "protectedsettings": {
                                        "Password": "[parameters('domainJoinPassword')]"
                                    }
                                }
                            }
                        ]
                    },
                    "osProfile": {
                        "computerNamePrefix": "[variables('namingInfix')]",
                        "adminUsername": "[parameters('adminUsername')]",
                        "adminPassword": "[parameters('adminPassword')]",
                        "windowsConfiguration": {
                            "provisionVmAgent": true
                        }
                    },
                    "licenseType": "Windows_Client"
                },
                "scaleInPolicy": "[parameters('scaleInPolicy')]",
                "platformFaultDomainCount": "[parameters('platformFaultDomainCount')]"
            }
        }
    ],
    "outputs": {
        "adminUsername": {
            "type": "String",
            "value": "[parameters('adminUsername')]"
        }
    }
}

The script includes the complete configuration of a Virtual Machine Scale Set (while using the Windows 10 Enterprise Multi-Session Image 1909), including the possibility to join the machines to the domain. After you replaced the initial code, with those from above it should look like this:

Press the “OK” button and click on “Add” to save it in the template library.

Attention! Before continuing to deploy that template we should go ahead and create a parameters file based on your needs! For this reason we go through the Virtual Machine Scale Set configuration as mentioned in the article one. The next steps describe how to proceed after you described the parameters that fit for your environment.


If you want to use a custom image, you need to replace the part “imageReference” from the ARM template with your own preferred image. I can understand that these manual steps might be tedious, but they´re a good way to start with the auto deployment and safes a lot of time later.

Here is an example how this looks like:

EXAMPLE using Win10 Enterprise Multi-Session 1909:                       
                       "imageReference": {
                            "publisher": "MicrosoftWindowsDesktop",
                            "offer": "Windows-10",
                            "sku": "19h2-evd",
                            "version": "latest"
                        }
EXAMPLE using custom image: 
                        "imageReference": {
                            "id": "/subscriptions/%YOURSUBSCRIPTIONID%/resourceGroups/%RESOURCEGROUPOFIMAGE%/providers/Microsoft.Compute/images/%IMAGENAME%"
                        }

Replace the values between % with your values:

  • SubscriptionID
  • Resource Group navigating to the image
  • Image name

If the scale set has been modified to your needs, click on “Download a template for automation”


From the next window, select download, to download your template and the parameters.json file, which will be used to import the settings into the recently created Template.

You will see that you´ve downloaded a ZIP file, which included two JSON files inside. You´ll only need to save the parameters.json, which includes all of your parameters defined and which can be used for this deployment.

Now we need to change back to our Template view, where we´ve created our SessionHost deployment template.

As a next step, we need to select our recently created template and click “Deploy” to start our deployment.


The following page will present you an empty template. Not very useful right? But for this reason we have our parameters.json, which we´re going to import right now! Click on “Edit parameters” in the top menu of the screen.

On the next page, select “Load file” and select the parameters.json stored on your local hard drive.

Recommendation from my side: Store it somewhere safe to not redo the steps of configuring the Virtual Machine Scale set multiple times. Once the parameters are successfully loaded, the result will look like this:

Click on “Save” below to import those values into your template. Looks already better right?

The first thing to select, is the Resource Group, in which our Scale Set should be created.

If you scroll to the end of this deployment template you´ll find the long awaited Domain Join capabilities!

Down below you can configure the following settings to join your hosts automatically to your domain:

  • Domain Name: e.g. wvdlogix.net
  • OUDN: Define the Organizational Unit as Distinguished Name convention
  • Domain and Username: IMPORTANT! NETBIOS\Username
  • Domain Join Password: Password of the corresponding user

If you´ve entered the required information, click on Purchase to get started and see the Session Hosts getting deployed inside your Virtual Machine Scale Set and joined to your domain afterward.

After approximately 15 minutes, the deployment should complete successfully and the created Session Hosts will be immediately visible from within the WVD portal and the OU addressed in the deployment.

Conclusion of solution 1: Virtual Machine Scale Sets

Like I mentioned in my previous article, even if we want to automate steps to get started with our deployment of Windows Virtual Desktop, there are a bunch of manual steps to perform. I asked myself so many times, why such a solution hasn´t been built in from the beginning? But on the other hand we need to be patient with Microsoft as WVD is evolving very quick.

Now we reached the final question of this chapter – For whom is a Virtual Machine Set a solution?

  • Environments with one custom Image to be deployed to the complete environment (can be used for multiple Host Pools yes, but administrative effort increases)
  • Demo environments

And to explain, what is necessary to have an automatic deployment of the Host Pool and cost reduction, we will have a look at, what Azure Automation can do for you.

Solution 2: Automatic Start / Stop behaviour of Session Hosts with Azure Automation

Back in our Azure Portal, we can go to one of our Resource Groups ( I suggest to create a separated one for Automation tasks – you´ll see later, why) In my case, I´ve created a RG called “KCLD-WE-WVD01-Automation and click on it.

Inside of the RG, we´re going to add a new resource to it, while clicking the “+Add” button.


Now we´re going to search for “Automation” from within the search box and select the Automation (Account) resource provided by Microsoft. Once selected, we click “Create” to get started.

On the next page, we´ll give our Automation Account a name, select the subscription, the RG we´ve created, and the operating location, which is West Europe in my case. You also have the possibility to create a RunAs Account, which is not needed for the solution, which we´re going to provision right afterwards. Hit “Create” once you´re finished.

After a few seconds, our storage account is ready to be used and we can go to the resource, which presents us the basic overview of an Automation account.

In here, we need to scroll down on the left hend side until we reach “Start/Stop VM”, which is the solution we want to install.

In the middle of the screen you see two options to choose, we´re going to Enable the solution first, so we hit the left “Learn more and enable the solution” option, which redirects to the Azure Marketplace again.


It´s obvious that we´re going to click on “Create”, but I´m very excited to see, what you´re saying about what this solution delivers additionally, when correctly configured. For now we click “Create” again to provision the solution.



The agent will guide us through the deployment of the solution, which requires the following components:

  • Log Analytics Workspace (Can be created from within the solution deployment)
  • Azure Automation Account (which has been created previously)
  • Parameters

After you´ve created the Log Analytics Workspace and select the Automation Account created by us earlier, we leave the parameters empty for the moment. To not hide the possibilities from you, here is the screenshot of the initial configuration, to be defined from over here already.

As mentioned before, we keep it empty and hit “OK” and click “Create”.

The deployment process is going to start and will finalize between 10-15 minutes.

Once finished, you can see that there have been 15 resources created, which is the reason why to separate the Automation resources from the computing once. The final outcome should look like this:

In the next step, we´re going into the Automation Account created and navigate to “Variables” to start configuring our Start / Stop solution.

The variables to be modified can be reviewed here:

Let me explain you what the variables are actually for:

  • External_AutoStop_Condition: Indicates if the value we specify in “External_AutoStop_MetricName” is “GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual” than the value we define in “External_AutoStop_Threshold”
  • External_AutoStop_Fequency: Anlayzes based on 5 minutes, what´s the current performance of the machines, based on the MetricName
  • External_AutoStop_TimeWindow: Defines the hours, for how long a machine is active, until it will be deallocated
  • External_ExcludeVMNames: Defines which machines aren´t deallocated (Suggestion from my side: At least one SessionHost should be defined here to prevent that no workspace is available if users need to work after business hours)
  • External_Start/Stop_ResourceGroupNames: Define the Resource Groups, where your machines are located.

Once this is filled out by us, we´re going to change back to the “Schedules” from within our Automation Account.

Now you will see, why I´ve suggested you to skip the configuration before. The timezone, while deploying the solution is configured in only one timezone UTC. If you go to schedules now, you can select your own timezone, which prevents you from doing a calculation issue during the timezone conversion. Here I can select, when VMs will be started and stopped.

Attention! If you want to scale based on metrics, turn off the Scheduled-StopVM, like this the Automation account will listen to the parameters defined before to deallocate the computing resource/s. If you just want the Start / Stop behaviour, keep the setting as is and just define the shutdown time properly.

Now we´re coming to the end of the configuration. Notifications!
Unfortunately the Start / Stop solution doesn´t include a user friendly notification message, especially not for the end users! I´m working currently on a solution for this and inform you ASAP once it´s ready.

Conclusion of solution 2: Automation Account with start / stop functionalities

Finally there are ways to automate at least a few deployment steps within Windows Virtual Desktop. As you can see from above that there are still a lot of manual tasks to go for, even if we want to use the Automation Account with the solution in a different way while modifying the variables to our needs and to metrics which fit best for us.

Last but not least, for whom should be this solution?

  • Organizations which have real virtual machines provided as Session Hosts
  • Have clearly defined after business hours or can work with limited resources at a certain time
  • Don´t need to inform the users about the unavailability of resources after the automation comes in place.

This was really one of my largest blog articles I´ve ever written. I hope you like it and can use some particular functionalities for your deployments.
As this was so much and you have maybe so many questions, I´m going to plan a Teams event in the near future, where you can ask all of your questions around Automation or WVD so far and maybe we, together as a community, are finding “best practices” for our future deployments.

Thanks a lot for reading and sharing,

Cheers,

Patrick

Please follow and like us:

One Reply to “Windows Virtual Desktop Host Pool Automation Part 2”