Shut Down Unused Session Hosts in a Windows Virtual Desktop Pooled or Personal Host Pool

WVD Function App

(UPDATED 6/17/2021 with code for multiple pooled and personal host pools) Azure Virtual Desktop (previously Windows Virtual Desktop (WVD)) has a new option in preview that starts session hosts in a personal or pooled host pool when a user connects. It won’t, however, shut down the session hosts when the user logs out. The script outlined in this video will evaluate all running and available session hosts in a personal or pooled host pool and shut down and deallocate session hosts without an active connection.  Deallocating Session hosts while not in use can save money on compute costs.

This script is intended to be used with the auto start on connect preview feature.  It uses an Azure Function with a system assigned managed identity to query AVD/WVD and shut down the Session Hosts.  It uses an Azure Function because support for a managed identity is GA, and the schedule can run more frequently than every hour.  The script could also run in an Azure Automation runbook.

Auto Start WVD Pooled and Personal Host Pool Session Hosts:
https://github.com/tsrob50/WVD-Public/blob/master/StopSH-MultiHostPools.ps1

One important step required for this script is a GPO, or some other method to set a time limit on disconnected sessions in the Host Pool.  The script will not shut down Session Hosts with any type of active session, including disconnected sessions. 

Also, I recommend not running the script more frequently than every 30 minutes.  There is a short window when a user connects, and the Session Host has started, but the login has not fully processed.  The script could interpret the Session Host as powered on with no active connects and shut it down. 

Disconnect GPO

Start by creating a GPO that sets a time limit for disconnected sessions and apply it to the Session Host OU.  The settings are located in:

Computer Configuration > Administrative Templets >
Windows Components > Remote Desktop Services > Remote Desktop Session
Host > Session Time limits

At minimum, enable “Set time limit for disconnected sessions” to a time limit that works for your organization.  Set it too high, and charges will accumulate for resources not in use, set it too low and end users may get frustrated when they step away for a short time and have to wait for their Session Host to start.

GPO Settings

Also, consider enabling “Set time limit for active but idle Remote Desktop Services session”.  Configuring that setting will disconnect a user who may have stepped away and left the remote desktop client open. 

Set the Max Session Limit

Auto Start on Connect will only power on a Session Host in a Pooled Host Pool if no Session Host are powered on or, if the powered on Session Hosts have reached the Max Session Limit. A Pooled Host Pool in Breadth-First Load Balancing requires a max session limit when used with the start on connect feature.

Create Function App

Let’s start by creating the function app.  From the Azure Portal, go to Function Apps and add a new function app.

Create a new Function App

In the Create Function App page, create a new or use an existing resource group and give the Function app a name.  Set the runtime stack to PowerShell core and set your region.  Once finished, it should look similar to the image below.

Create Function App Settings

Hosting, Monitoring, and tags can be left as default.  Go to Review and Create to deploy the Function App.

Go to the resource when finished.

Go to Resource

Configure the Function App

Next, we will configure the function app to use the PowerShell Az. Module and configure a System Assigned Managed Identity.

Configure Az Module

From the Function App, go to App files under Functions.

App Files

From the dropdown, select the requirements.psd1 file and remove the “#” from the line: ‘Az’ = ‘5.*’.

Requirements.psd1

Click Save and go back to the Function App Overview.

From the overview page, restart the function app.  This will apply configuration changes we just made.

Restart Function App

Configure the Managed Identity

Next, we’ll configure the managed identity used by the Function App to interact with resources in the subscription.

Go to Identity under Settings in the Function App.

Settings Identity

From the System Assigned Identity page, change the status to On and save, that will create a new identity, and the Object ID will display similar to below, without the blur.

System Assigned Identity

Next, we give the identity rights to view properties of the WVD environment and manage virtual machines in the subscription.  Go to Add Role Assignments.

From the Add Role Assignments page, click Add Role Assignment (preview).

Add Role Assignments

On the next page, set the Scope to Subscription, Verify the correct subscription is added, and search for and select Desktop Virtualization Reader.

Add Roles

Click Save to add the Desktop Virtualization Reader role. 

Follow the same steps to add the Virtual Machine Contributor role.

Virtual Machine Contributor

Once finished, go back to the Function App.

Create the Function

Next, we’ll create the Function.  A Function App can have many functions of the same type.  For example, we can have multiple PowerShell Functions in this Function App. 

From the Azure Function App, go to Functions under Functions.

Functions

Click Add to add a Function.

Add a Function

From the list of templates, select Timer Trigger.   This will trigger the Function based on a schedule.

Timer Trigger

Go to Template Details, give it a new name if needed, then change the schedule by replacing the 5 with a 30.  This will set the timer to run every 30 minutes on the hour.  You can change this to a different frequency, 45 to run every 45 minutes for example.

Function App Schedule

See the link below for more details on scheduling for a Function App.
https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer?tabs=csharp&WT.mc_id=AZ-MVP-5004159#ncrontab-expressions

Click Add to add the function app.

From within the Function, go to Code + Test under Developer.

Code + Test

If you need to change the schedule, go to the Function.json file in the Function drop-down.

Function json

Change the schedule as needed.

Go back to the Run.ps1 file and replace the existing code with the StopSH-MultiHostPools.ps1 code. The code can be found here:
https://github.com/tsrob50/WVD-Public/blob/master/StopSH-MultiHostPools.ps1

Go to the Variables section.  Update the Host Pool name and Resource Group for the Personal or Pooled Host Pool. 

If the function is running againt multple Pooled or Personal Host Pools, copy the block of code from the commends and paste it imediatly after the block updated in the previous step.

Host Pool Code Block

Add a new block of code for each host pool targeted by the function. When finished, the variable section will look similar to the code below.

Two Host Pools

When the Function runs, a while loop is used for each host pool. It starts by getting all the active sessions in the Host Pool.  It then creates a list of all active Session Host names, using a for each loop to remove duplicate values

Get Active Session Hosts

Next, it creates a list of all Session Hosts that are powered on and not in drain mode.  Setting drain mode will exclude the Session Host from the script. This way, if a Session Host has to be on for a few hours for maintenance, such as to pick up software updates, the script won’t shut it down.

It then compares the Session Hosts that are powered on with the Session Hosts that have active connections.  If the Session Host is on but has no active connections, it’s shut down.

Shut Down Session Hosts with no Sessions

After that, the script loops through the next instance of a Host Pool until the end.

Once updated with your Host Pool and Resource Group information, save the Function.

Run the Script

Test it before you trust it!

My lab had three Session Hosts. All were powered on and available with two users logged in.

Test Pool Before Script Run

Open logs in the Function so we can see the activity.

Open Logs

Check the time!  As configured in this example, the script will run every 30 minutes on the hour.  Close to run time, you can wait for it to run on its own. 

Otherwise, go to Run/Test and click Run to start it manually.

Test Run
Run the Function Manually

Close the Run window after it starts.

The Function will take a couple of extra minutes to finish on the first run as it has to download and add the PowerShell Az module.

Download PowerShell Az Module

Once it finishes running, the output will indicate any Session Hosts that are shut down in the function output.

Function Output

My example now shows one Session Host is off and unavailable.  With Start VM on Connect enabled, the Session Host will start the next time an assigned user logs in.

Session Host Unavailable

That, my friends, is how to shut down Session Hosts in a Personal Host Pool with no active sessions. 

8 thoughts on “Shut Down Unused Session Hosts in a Windows Virtual Desktop Pooled or Personal Host Pool

  1. Great stuff! Now that autostart on connect is available for pooled hostpools as well, could we adapt this for that use case?

  2. Hi Travis! Wonderful script! I am using in a Pooled, but I had a little problem with the name of the VM. In the Pooled Host Pool, the VM Name is not the ComputerName because it uses a prefix name of the host pool. When the script tries to shutdown the VM, It is using the ComputerName. (—> (($sessionHost).name). I dont know if this mess is my fault, because I didnt use the computer name in my AD with the prefix name of VDI. So, Windows VDI shows my AD ComputerName in the Host Pool, but, outside of the VM, that is what care for the script to shut it down… it does not works.
    Is there a way to bring convert my Computer Name to the VM Name ? Please, if you can…. let me know (I would like to contribute ($) if you could help me! Thanks!

Leave a Reply

Your email address will not be published.

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