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

26 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

      1. Thanks for your answer. I appreciate your suggestion on the other script. We are already using it but that is for Fall-2019. We want to migrate to Spring-2020..
        I also think, even though your script is simpler it can be applied to a broader scenario (with some minor variations). Your script will start/stop at any time depending on the number of sessions That other script is limited to starting host during peak time and turning off during non peak time. If for some reason your load goes up after hours or weekends it will not scale up.
        I agree with you, if money is the “main” concern depth-first is the winner. But, we are looking for a balance between performance, user experience and cost savings. We think depth first is not the right solution for us given some particular needs. As far as draining, we don’t want to force users to logoff. Just let users disconnect/logoff on their own. The GPO will take care of closing the sessions as they disconnect. Just as you do on your script. We may only need the drain to prevent users from logging in while shutting down which you have less probability on the depth-first as you suggested.. Maybe just setting drain mode on right before shutting down and then back on right after would do it (Just a thought. As you said some additional logic could be needed).
        In any case, I just thought there was no need to limit the script to just one load balancing mechanism as long as you understand the risks and your particular use case.. Great script, simple and functional.

        Thanks,

  6. Hi Travis,

    Great work – thank you for sharing it.

    Will the script shutdown all VMs when there are no active users or will leave one VM running ready to accept connections?

  7. Hi Travis ,

    I have couple of questions , i am bit confused about max threshould ,/minimum session threshould.

    Let me tell you our environment example :

    Example 1: In our environment i have deployed 2 wvd in host pool with configuration : Standard D8s v3 (8 vcpus, 32 GiB memory) .

    10- 15 people will be using it in peak hours and 2 guys will use in off peak hours

    Max host pool session configuration is 21 .

    Example 2 :

    i have deployed 4 wvd in host pool with configuration : Standard D8s v3 (8 vcpus, 32 GiB memory) .

    35 -40 people will be using it in peak hours and 3- 4 guys will use in non peak hours

    Max host pool session configuration is 50

    What setting u recommend me to set for peak hours / non peak hours so your scripts calculation works perfect for me.

    Also how we can achive if 0 session are logged in all 2 will get shutdown and when anyone tries to connect to wvd it will power ok the machine ? If this is not possible then how we can shift left this activity to service desk team which option i need to choose in access control so that they only can do power on power off wvd.

    Note : GPO is already deployed for kill the disconnected or inactive session.

    1. Hello

      Thanks for the question. To start, this solution is designed to only use Depth First. Depth first will fill one session host to the max number of connections before moving sessions to the next session hosts. As users log in and out, new sessions are directed to the session host with the most number of users up to the maximum session limit.

      If the max session limit is 21 with 10-15 users, the second session host would not be used. If you change that to 10, the 11th connection would go to the second session host.

      The script has a Server Start Threshold. This sets the spare capacity that should be available between script runs. It’s a spare capacity setting to accommodate new logins. It essentially turns on new session hosts once the active ones get close to full.

      For 40 users and 4 VM’s for example, setting the Max Session Limit to 10 would accommodate 40 users. If the Server Start Threshold was set to 2, a server would start after the 9th, 19th and 29th login.

      The peak hours is to accommodate more aggressive logins during busy times. In the previous example, if the peak time server start threshold was set to 10, one extra server would be turned on.

      As for shutting down all session hosts, I have heard some people will set the server start threshold to 0 during normal hours and above 0 for peak or office hours. This will shut down all session hosts as users log off. Once the last one is down, no one will be able to log in until the peak time when a session host starts again.

      I hope this helps.

      1. Thanks a lot . can you please tell me how we can change the MAX session limit on non- arm WVD . Which PowerShell command we need to use ?

        Example: Suppose for Standard D8s v3 (8 vcpus, 32 GiB memory) VM ( NON – AR ) . It will wait till the session reaches to 24 ( its taking default values based on VM size I guess ) and then only it will spin up a new WVD . How we can set up a limit as per our convenience ?

  8. Thank you so much for this. Works great! I have one question. Can the script be modified to start or stop more than one session host? I am using a large shared pool of Windows 7 (I know, but the client needs it) with the max sessions set to one. The script works exactly as expected, but will only start/stop one session host per run. For example if I have the threshold set to 50 and there are 25 hosts running with one session (the max) each, the pool is 25 sessions short. Each time the script runs, it will only start just one host. The new logins can easily overrun the script. Thank you in advance for responding.
    -Tony

  9. Hi Travis,

    If I have 2 host pools inside 2 differents Resource Groups, I have to create 2 different functions or I have to include inside the script function like example below?

    Thank You

    Example:

    # Host Pool Name
    $hostPoolName = ‘WVDHP1, WVDHP2’

    # Session Host Resource Group
    # Session Hosts and Host Pools can exist in different Resource Groups, but are commonly the same
    # Host Pool Resource Group and the resource group of the Session host VM’s.
    $hostPoolRg = ‘RGHOSTPOOL1, RGHOSTPOOL2’
    $sessionHostVmRg= ‘RGHOSTPOOL1, RGHOSTPOOL2’

  10. This is brilliant – thank you so much for all your hard work!

    I found a typo on line approx 88:
    if ($hostsToStart -gt $offSessionHostsCount) {
    $hostsToStart = $offSessionHosts
    }
    Should be:
    if ($hostsToStart -gt $offSessionHostsCount) {
    $hostsToStart = $offSessionHostsCount
    }

    Also, my work’s peak time is overnight so $startPeakTime is PM and $endPeakTime is AM. I added a comparison to find this and adjust for it:
    if ($startPeakTime -gt $endPeakTime -and $dateDay -in $peakDay) {
    Write-Verbose “Peak hours are overnight, adjusting peak times”
    Write-Verbose $startPeakTime
    $endPeakTime = $endPeakTime.AddHours(24)
    Write-Verbose $endPeakTime
    }
    Not sure if you wanted to add this to your template.

    I am so grateful for your work, this is so very cool. Thankyou thankyou thankyou.

  11. Hi Travis,

    Thank you for the great article. I have tried to configure this and getting bellow error even though I have imported all the required modules. Would really appreciate if you have any insights about it.

    “ERROR: Get-AutomationVariable : The term ‘Get-AutomationVariable’ is not recognized as the name of a cmdlet, function, script file, or operable program.Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

    1. Hello, verify you are using the script with the name WVDARM_ScaleHostPoolVMs.ps1. You may have used the version for Fall 2019 that uses Azure Automation. I updated the comments in the read me so the difference between the two are more clear.

      Thanks,
      Travis

      1. Hi Travis,

        Thank you for the prompt response. Yes, you are correct. I was using the old script. Thank you again.

        I have a last question. If I am using 2 session hosts in a single host pool, is it possible to shut down both the session hosts if there is no any active sessions?

Leave a Reply

Your email address will not be published.

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