Auto Start and Stop Session Hosts in Windows Virtual Desktop Spring Update (ARM) Edition with an Azure Function

I am happy to release an update to my Windows Virtual Desktop (WVD) Start-Stop script for Windows Virtual Desktop updated for Spring 2020, or “WVD ARM.”  This script uses an Azure Function to starts and stops WVD Session hosts in a host pool based on the user load. 

This article and script apply to the WVD ARM, or Spring Update in Public Preview as of June 2020.  You can find information on my previous version here.

The GitHub page with the Function App code is located here:
https://github.com/tsrob50/WVD-Public

An overview of deploying the script is available in this post.  A full overview and deployment walkthrough is located here:

Below is a list of the changes made to the updated script:
PowerShell Module – Refactored to use Az.DesktopVirtualization PowerShell module instead of the Microsoft.RDInfra.PowerShell module.
100% Function App – the Az.DesktopVirtualiztion module supports PowerShell Core.  The script now runs as a PowerShell Function App.  No more Azure Function, Azure Automation Mix.  This includes using a System Managed Identity to start and stop the VM’s.
Updated Start/Stop code – Thanks to Kandice Hendricks for updating the start and stop functions in the previous version.  Those changes are part of this script.  The script will start and stop multiple VM’s per run. 
Updated Peak Time Code – I’m not sure how I ever got this to work.  The script now excepts a time zone instead of a UTC offset.  The correct time is calculated, including DST.

The following is a list of steps to deploy and modify the Function App to run the start stop script.  A host pool should be in place prior to configuring the script (see my video on setting one up for more information).  This script works with one host pool.  Deploy multiple functions within the Function App to managed multiple Host Pools.

Set the Auto-Logout GPO

I start with a GPO that will log out disconnected and idle sessions and apply it to the Session Host OU.  This step could be skipped, but idle and disconnected sessions will prevent session hosts from shutting down.  Be sure to set timeout values to a setting appropriate for your environment.

From an account with privileges to create and manage a GPO, go to Windows Administrative Tools, Group Policy Management.  Create a new GPO and give it a name.  Right click and Edit the GPO.

GPO Object

Modify the settings under Computer Configuration > Policies > Administrative Templates > Windows Components > Remote Desktop Services > Remote Desktop Session Hosts > Session Time Limits.  Enable and set the limit for disconnected sessions and limit for active but idle RDS sessions.

Edit Time Limit

Apply this GPO to the session host OU in Active directory once created.

Create the Function App

This step creates a PowerShell based Function App on a Consumption plan.  Modify as needed if you have an app service plan in place already.

Go to the Azure Portal and Create a Resource.  Search for Function App and select Function App.

Create Function App

Click Create at the Function App page and fill out the information in the Basics section.  It should look like below once finished.

Select an existing Subscription and existing Resource Group, or create a new Resource Group.
Give the Function App a name.
Leave Publish as Code.
Change the Runtime Stack to PowerShell Core.  Leave the version as 6.
Change the region.  Best to select the same region as the Session Host Pool.

Function App Bsics

Click next to Hosting.

In hosting, leave the storage account to New, the Operating System to Windows.

Set the Plan Type to Consumption.  Use Premium or App Service Plan if your environment requires the added functionality.  Be aware that all options have a monthly fee associated with them.  The Consumption plan is not free but costs the least.  See the link on the deployment page for more details.

Function App Hosting

Once finished, click next to Monitoring.

Leave Application Insights enabled and create a new Application Insights instance.  Alternatively, you can select an existing if you use Application Insights in your environment.

Function App Monitoring

Click Next to Tags.  Add Tags as needed for your environment.

In Review and Create, Click Create to deploy the Function App.

Function App Deployment

Create the Managed Identity

A managed Identity provides an identity that the Function App will use to interact with the Session Hosts.  A Managed Identity is a special identity with a life cycle that matches the Function App.  The function app uses a System Managed Identity to start and stop VM’s.  When the Function App is deleted, the Managed Identity is removed with it.

Go to the Function App once the deployment has finished. Click on Identity under Settings.

Managed Identity

From System Assigned Managed Identity, change the status to On and click Save.

A verification box will appear.  Click Yes to create the Managed Identity.  Note the name in the box; this is the name of the identity that will get RBAC rights to the Session Host Resource Group.

Enable System Managed Identity

The Object ID will display once the save has finished.

Managed Identity Object ID

Next, go to the Session Host VM Resource Group.  It is possible to deploy Session Host to a Resource Group different from the Windows Virtual Desktop Host Pool.  Be sure to set RBAC permissions on the VM Resource Group.

Open Access Control (IAM) from the Resource Group.

RBAC Access Control

Click Add a Role Assignment

Add a Role Assignment

Set the Role as Contributor and select the Managed Identity setup in the previous step.  Click Save when finished.

Save Role Assignment

Go back to the Function App, Identities, and click Azure Role Assignments.  The new role shows in the function app.

Azure Role Assignments

Create the Function

Now that the Function App is in place and has permissions to the Resource Group, we can move onto creating the Function and adding the code. 

From the Function App, go to Functions.

Create Function

Click Add to add a new Function.  Select Timer Trigger.

New Timer Trigger

Give the new Function a name and leave the schedule as is.  The new timer trigger defaults to run every 5 minutes. 

Create Timer Trigger Function

The Overview page displays once created.  Go to Code + Test to view and change the code.

Code and Test

Delete everything below param($Timer).  The param($Timer) line has to stay in the code for the Function App to run.

Function App Code Page

Next, go to the GitHub Repo and copy the script.  The code can is located here Click the Raw option to view and copy the code without formatting.

GitHub Raw

Paste the script into the Function App Code page under the param($Timer) line like below.

Paste Code of Function App

Next, go to the Variables section and make the following changes:

VerbosePreference – Verbose output was added for initial testing.  Set to “Continue” to see the output in the logs or “SilentlyContinue” to suppress the verbose information.

ServerStartThreshold – This is the spare session capacity the host pool has available between runs.  If the number of available sessions is less then this amount, a server will be started.  If the number of available sessions is greater than this amount, a server will be stopped.

UsePeak – Peak will modify the threshold value, so more sessions are available during peak business hours.  Set this to “Yes” if using and modify the peak threshold, start and stop time, time zone and Peak days as shown in the code.

HostPoolName – The name of the host pool to monitor.

HostPoolRG and SessionHostVmRg – the Host Pool Resource Group and the Session Host VM Resource Group.  These are typically the same but could be different.  Find the Host Pool Resource Group in Windows Virtual Desktop under Host Pool

Host Pool Resource Group

Host Pool Resource Group

Session Host VM Resource Group from Virtual Machines in the Azure Portal.

Session Host VM Resource Group

Once finished, click Save at the top of the Code + Test Page

Save Code

Test the code by running it with the Test/Run button (shown above)

At the Input Output Screen, click Run at the bottom.

Run the Code

The Function should show a 202 HTTP response indicating it was accepted. Close the Run window once accepted.

202 Response

That’s it, the script is in place and running.  Below are some tips for managing the function app now that it’s deployed.

Disable the Function App

I like to shut my lab session hosts down when not in use.  This script will start them back up (that’s what it’s for!).  To disable the script, to go to the Functions Overview windows and click Disable.  This will stop the schedule, but it can still be run manually.

Disable Schedule

View the Logs

Go to Monitor in the Function.  This shows the log of each run.  Note that it can take up to 5 minutes for the logs to show up after a job runs.  If Verbose is enabled, those messages will show up here, along with information and error messages.  Verbose Logging is enabled by changing the $verbosPreferance in the script to “Continue” and editing the host.json file.

To enable Verbose Logging, go to the Function App, App Files, and select host.json.  Add the section of code starting with “logging” to the host.json file. (Be sure to add the comma as pointed out in the image below).

"logging": {
    "logLevel": {
      "default": "Trace"
    }
  }
Logging host.json

Click Save to finish

Change Run Interval

The default run interval for a time trigger is 5 minutes.  Set the time interval with a Cron expression in the function.json file.  Information on how to format a Cron expression is in the Functions readme.md file.

readme.md Cron

To change the schedule, modify the Cron expression in the function.json file. 

function.json

12 thoughts on “Auto Start and Stop Session Hosts in Windows Virtual Desktop Spring Update (ARM) Edition with an Azure Function

  1. I get error: Cannot bind argument to parameter ‘SubscriptionId’ because it is null

    [Error] ERROR: _TimerTrigger1_ : Error getting host pool details: Cannot bind argument to parameter ‘SubscriptionId’ because it is null.
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,_TimerTrigger1_

    Script stack trace:
    at , D:\home\site\wwwroot\TimerTrigger1\run.ps1: line 156

    Microsoft.PowerShell.Commands.WriteErrorException: Error getting host pool details: Cannot bind argument to parameter ‘SubscriptionId’ because it is null.

    2020-06-19T09:49:14Z [Information] Executed ‘Functions.TimerTrigger1’ (Succeeded, Id=ffb0bf3b-1bf1-414d-a1b6-0b3b416aa56f)
    2020-06-19T09:50:00Z [Information] Executing ‘Functions.TimerTrigger1′ (Reason=’Timer fired at 2020-06-19T09:49:59.9974449+00:00’, Id=fa2e49d5-970a-46cc-b0d5-71ed7211c611)
    2020-06-19T09:50:00Z [Error] ERROR: _TimerTrigger1_ : Error getting host pool details: Cannot bind argument to parameter ‘SubscriptionId’ because it is null.
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,_TimerTrigger1_

    Script stack trace:
    at , D:\home\site\wwwroot\TimerTrigger1\run.ps1: line 156

    Microsoft.PowerShell.Commands.WriteErrorException: Error getting host pool details: Cannot bind argument to parameter ‘SubscriptionId’ because it is null.

    2020-06-19T09:50:00Z [Information] Executed ‘Functions.TimerTrigger1’ (Succeeded, Id=fa2e49d5-970a-46cc-b0d5-71ed7211c611)
    2020-06-19T09:49:14Z [Error] ERROR: _TimerTrigger1_ : Error getting host pool details: Cannot bind argument to parameter ‘SubscriptionId’ because it is null.
    + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,_TimerTrigger1_

    Script stack trace:
    at , D:\home\site\wwwroot\TimerTrigger1\run.ps1: line 156

    Microsoft.PowerShell.Commands.WriteErrorException: Error getting host pool details: Cannot bind argument to parameter ‘SubscriptionId’ because it is null.

    1. Had the same error, the managed identity and roll assignment was missing,
      after fixing this everything was running as expected

  2. I appreciate your hard work.. It’s really amazing.

    I have query related to “Updated Peak Time Code”, does it execute based current Region where session hosts is hosted , for example US EAST
    Or it execute based on the time set on VM or does it execute based on the current time zone from where you are executing – for example: I’m located in India and hosted session hosts in US EAST, so can I set India time zone or US EAST or Timze zone set on Server.

    2. Is there any script or software which tell the user what is the current bandwidth from their logged in VDI desktop..

  3. Thanks, Travis

    Can you let me know how to roll out newly updated Golden image to Session hosts?

    I see there is way using shared image gallery, but getting error while rolling out to session hosts (after completing the rolling out shared image version of Image, VM shows “Unavailable”

    1. The PeakServerStartThreshold is the number of user sessions that should be available between each run, not the number of session hosts that should stay running. By design, this script will try to minimize the number of servers running. With Depth-first load balancing WVD would not allocate sessions to a new server until one with active connections hits the max session count. You could keep an extra server running by durring peak by setting the threshold to the max session count +1.

  4. Hello Travis,

    Can I know how to start the same session host at different time zone. For example: user from the same organization, want to login to a session host from the different Time zone. so I want to do auto start and auto stop.

  5. Hello Travis,

    I watched the video and looked at the script. The reasons you mentioned for it to work “only” with the Depth First were around costs, not technical. On the math equations you are always looking at the total amount of session on all hosts not individually. Looking at the script I do not see a reason for it not to work with Breadth First other than manually specifying a MaxSession value. Other than cost, Is there any technical reason why the script would not work with Breadth first?.

    Thanks

    1. Depth first will direct new clients to the host with the most connections, up to the maximum connection. This acts to consolidate clients to a minimal number of hosts. This is a more economical way to handle sessions, both in money and in compute utilization. Breadth-first distributes new connections across all available hosts, making it difficult to target session hosts to shut down. That would require additional logic to put session hosts in drain mode.
      Microsoft has a similar script that works with Breadth-first and Depth-first. I originally build my script as a simpler alternative to this one. If you are using Breadth-first, it may have the logic you need. https://docs.microsoft.com/en-us/azure/virtual-desktop/virtual-desktop-fall-2019/set-up-scaling-script

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.