Add Sessionhosts to your existing RDS deployment using ARM templates and Invoke-AzureRmVMRunCommand
Introduction
If you are using ARM-templates & Desired State Configuration (DSC) for your RDS deployments, you will be able to cover almost all your needs. But you could also use different options or technologies for your needs.
A great example is the combination of an ARM template with the CustomScriptExtension (for installing the RDS Sessionhost role on the VMs), the JsonADDomainExtension (to join the VM to a domain) and the new Invoke-AzureRmVMRunCommand cmdlet.
In this blog, I will show you how you can use ARM templates & the new Invoke-AzureRmVMRunCommand cmdlet to add new sessionhosts to your already existing RDS deployment, and more!
Note: I know this is not the only way to do this. As we say: All roads lead to Rome. You have to decide for your scenario which road you want to take.
The Invoke-AzureRmVMRunCommand cmdlet
The new cmdlet allows you, as an admin of the Azure subscription, to “Invoke a run command on the VM”. Run Command uses the VM agent to run PowerShell scripts within an Azure Windows VM. This means that the script will be executed as the Local System account (important to remember). There are some restrictions to the cmdlet, which you can find here.
Most important thing to remember: the Powershell script you provide to the cmdlet is copied to the VM, and then executed under the Local System account.
The goal
The goal of this blog is to do these tasks:
- Create a new VM using an ARM Template
- Install the RDS Sessionhost role on the new VM using the CustomScriptExtension
- Join the new VM to a domain using the JsonADDomainExtension
- Add the new VM to an existing RDS deployment, create a new SessionCollection with the new VM, and set some basic SessionCollection settings using the Invoke-AzureRmVMRunCommand.
Because of all the great blogposts and examples on ARM Templates & RDS deployments (a great example: part 1 & part 2 up to part 7 from Freek Berson), I will not go deep dive into this. I will touch the most important sections and parts of the template to complete tasks 1-3. Task 4 will be the main topic of this blog.
Prerequisites
Before you can start with any of this, you will need your basic setup. You will need to have a VNET, subnet(s), an Active Directory setup and a full RDS deployment with RDS Connection Broker, Gateway & WebAccess in place.
Task 1-3: Create VM, install RDS Role & join to the domain.
Create VM using ARM Template
To create a VM on Azure, you have a lot of possibilities: using the Azure Portal, using Powershell, using Azure CLI, etc etc. Another great option is using an ARM Template. As I said before, there are so many great blogposts and examples, so I’m not going into detail about this. Below, you can find a screenshot from my ARM Template which will be used for the next parts as well.
Install RDS Role using CustomScriptExtension
In the ARM template, you can already start customizing your VM to your needs. This can be achieved using the CustomScriptExtension.
You first create a Powershell script. For this blog, I created a script with the following content:
Set-NetFirewallProfile -Profile "Domain","Public","Private" -Enabled False
Import-Module "ServerManager"
Add-WindowsFeature -Name "RDS-RD-Server" -IncludeAllSubFeature
Add-WindowsFeature -Name "Desktop-Experience" -IncludeAllSubFeature
This will install the RDS-RD-Server role required to add to our RDS Deployment.
Next, you will need to store this script on an Azure Storage Account so it can be used in the ARM Template.
To link a CustomScriptExtension to a VM, you define it in the Resources section of the VM. You add a resource from the type “Microsoft.Compute/virtualMachines/extensions”. (line 192) You give the resource the name from your VM and add a name for the action. In this case I named it Fix-RDS (line 193). The script information is located in the “properties” section. First, you set the extension type to “CustomScriptExtension” (line 203). Next, you specify the script Uri (line 208): this is the Uri from the Azure Storage Account, the blob container and filename (see previous screenshot). Next, you will enter the command that needs to executed (line 212) Last part is the security information to download the Powershell script. In the example, I’m using the StorageAccountName & Key (line 213 & 214).
Join to the domain using JsonADDomainExtension
Once the VM is created in Azure, you want to be able to access the VM directly. This can be made easier if the VM is directly joined in your existing Active Directory. This can be done using the JsonADDomainExtension.
To link a JsonADDomainExtension to a VM, you use the “Microsoft.Compute/virtualMachines/extensions” resource again, but this is not linked under your VM section. It’s a separate section, not linked or under the VirtualMachine section as you can see in the screenshot below. You give the resource the name from your VM and add a name for the action. In this case I named it “joindomain” (line 229). First, you set the extension type to “JsonADDomainExtension” (line 233). Next, you enter the Active Directory name & the FQDN from the user to perform the join. This user needs to have permissions to perform the join in your AD. Last part is the password from that user (line 244), but this is entered in the protectedSettings part for security purposes.
Now you have an ARM Template that completes Task 1 till 3.
Task 4: Add the new VM to your existing RDS Deployment.
All previous steps were all done using an ARM Template. This template deployment can be started from the Azure Portal, Visual Studio (Code) or using Azure CLI. But it can also be started from a Powershell script using New-AzureRmResourceGroupDeployment.
And once you have started your deployment, the next step is easy. You wait until the deployment is finished using the Job ID.
As soon as the deployment is finished, you can further customize your VM, and finish the RDS setup.
The command
Invoke-AzureRmVMRunCommand has a few required parameters:
- ResourceGroupName: the resource group where the VM is located in.
- CommandId: The type of command you want to execute. In my example, I’m going to use “RunPowerShellScript”
- VM / VMName / ResourceID: the VM where the command needs to be executed on.
When using “RunPowerShellScript” as CommandId, you will need to specify which script needs to be executed. Therefor, you use the ScriptPath parameter. Here, you specify the path where the script is located. Important: the script needs to be on the computer/server executing the Invoke-AzureRmVMRunCommand, so you need to specify the local path on the local computer/server.
Last part are the optional parameters that you need to provide to your own script. You can create a hashtable containing all your parameters for your script and provide the hashtable to the cmdlet.
In my example, I execute the command on the Connection Broker. This is the most ideal VM I think to do this, because you are sure the necessary cmdlets are installed.
$rdsScriptParameters = @{"serverNames" = "MICHA-P-RDH-001.asi.hosting"; "sessionCollection" = "`"Micha Demo Collection`""; "sessionCollectionDesc" = "`"Micha Desktop Demo`""; "sessionCollectionUserGroup" = "MichaDemoGroup"; "connectionBroker" = "ASI-I-RDB-001"}
Write-Host "Adding RDS Server to Connection Broker, Create new RDS Session collection and grant permissions to new User Group..." -ForegroundColor Yellow
Invoke-AzureRmVMRunCommand -ResourceGroupName $resourceGroupName -Name "ASI-I-RDB-001" -CommandId 'RunPowerShellScript' -ScriptPath "AddRDServer.ps1" -Parameter $rdsScriptParameters
There are a few important things you need to remember!
- If you want to use blanks in your parameters, you must use double quotes and escape them, as you can see in my example below. So for example: you want to pass the parameter “sessionCollection” with the value “Micha Demo Collection”, then you need to add an item to the hashtable like this: “sessionCollection” = “`“Micha Demo Collection`””
- Your script can contain parameters, but the parameters cannot be set to manditory. So your parameter cannot have this property: [Parameter(Mandatory=$true)]. Otherwise, you will end up a vague error like you can see below. I already posted this into the Powershell Advisor group to see if this can be fixed. Invoke-AzureRmVMRunCommand : Long running operation failed with status ‘Failed’. Additional Info:‘VM has reported a failure when processing extension ‘RunCommandWindows’. Error message: “Finished executing command”.’ ErrorCode: VMExtensionProvisioningError ErrorMessage: VM has reported a failure when processing extension ‘RunCommandWindows’. Error message: “Finished executing command”.
The script
Now comes the hardest part, because as I said in the beginning, the command started by Invoke-AzureRmVMRunCommand is executed on the VM under the Local System account! As you may know, when you execute the Add-RDServer cmdlet, the cmdlet will check if the role you specify is installed on the new VM. If it’s not installed, the cmdlet will try to install the role. And the Local System account of your Connection Broker does not have permissions on the new VM.
So how do we fix this?
As stated in the beginning, the script is copied to the target VM. And the easiest way to perform and control a RunAs, is through a Scheduled Task.
So here is the solution:
- Create an inner script inside the outer script
- Output the inner script to the target VM disk
- Create a Scheduled task to run the inner script as a user with enough permissions
- Start the task and wait.
The outer script is the script you have on your local computer, containing parameters, the inner script, the scheduled task creation part and the wait job.
Sounds complicated, but really isn’t. Let’s go over it step by step.
The Parameters
The outer script starts with the parameters you want to provide. In my example, it is just these simple parameters:
param (
[string]$serverNames,
[string]$sessionCollection,
[string]$sessionCollectionDesc,
[string]$sessionCollectionUserGroup,
[string]$connectionBroker
)
The Inner script
We want to output this inner script to a ps1-file, without the outer script to execute the inner script. The easiest way to do this is by using a Here-String in Powershell. You create a variable, and start with @". All the text following after this, is interpreted by Powershell as text in 1 variable, no matter how many lines or tabs you enter, until you end it with “@
So for this script, I add the basic commands to add a server to an existing RDS deployment. I also added a screenshot to show you how it looks in Powershell ISE.
$script = @"
`$serverNames = "$serverNames"
`$serverNames = `$serverNames.split(',')
`$sessionCollection = "$sessionCollection"
`$sessionCollectionDesc = "$sessionCollectionDesc"
`$sessionCollectionUserGroup = "$sessionCollectionUserGroup"
`$connectionBroker = "$connectionBroker"
Import-Module RemoteDesktop
ForEach (`$serverName in `$serverNames)
{
Add-RDServer -Server `$serverName -Role RDS-RD-SERVER
}
New-RDSessionCollection -CollectionName `$sessionCollection -CollectionDescription `$sessionCollectionDesc -SessionHost `$serverNames
"@
Output the script
Easiest part: Just pipe your script variable to the Out-File cmdlet. This will write the inner script to the local disk of the server.
$script | Out-File -FilePath "C:\Temp\addRDServer.ps1"
The scheduled task
A bit more difficult, but once you get it, it’s easy as 1-2-3. First you create “an action”. This is what the Scheduled Task is going to execute. In my example: the Inner script in Powershell that we just created on the VM(line 2). Next, you can specify some settings. In my example, I set the Compatibility level to the highest. So on a Server2016, this will be Server 2016 level (line 3). Last part is registering the Scheduled Task, specifing the user credentials, the action and the settings created just before that (line 4)
$taskName = "AddRDServer"
$Action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -File C:\Temp\addRDServer.ps1" -WorkingDirectory "C:\Temp\"
$Settings = New-ScheduledTaskSettingsSet -Compatibility Win8
Register-ScheduledTask -TaskName $taskName -User "[email protected]" -Password "*********" -RunLevel Highest -Action $Action -Settings $Settings
Start the Task and Wait
This is easy, especially when you work with variables for your TaskName. Now you simply execute the Task using Start-ScheduledTask. And then you query the status from the Task until it is Ready
Start-ScheduledTask -TaskName $taskName
while ((Get-ScheduledTask -TaskName $taskName).State -ne 'Ready') {
Start-Sleep -Seconds 2
}
That’s it!
Conclusion
Invoke-AzureRmVMRunCommand is a great way to finish a ARM Template deployment. Because if you can start both from Powershell, you know which servers are created, and you can easily finish installations, RDS deployments, etc.
This is not the only way to do this, but’s a great way, easily manageble and highly automatable.
Comments
Previous Comments
casualscriptingguy | October 8, 2019 at 12:55 pm |
---|---|
Thanks, this literally saved my life. While being kept hostage I was offered to go free only if I was able to automate GPO importing into an Azure vm. Thanks to this blog I could do it! |
Arshad Naeem | November 1, 2019 at 6:26 am |
---|---|
Hi, |
|
Micha Wets | December 10, 2019 at 8:32 pm |
Hi Arshad, |
Enrika | February 21, 2020 at 2:19 am |
---|---|
Man, you saved my life, too, buddy. 🙂 I have to say, this is far and away the most unbelievably helpful and useful post I’ve encountered over the last few weeks of trying to implement a solution for the task I was given. There’s so much meat in here, and although my task was different (I had to look up what an RDS deployment is 🙂 ), I imagine the examples and instructions in here could be utilized to solve nearly any problem involving the deployment of an Azure VM with an ARM template, especially a problem involving a lack of permissions on the newly-created machine. Honestly, I thought right up until yesterday that it might simply not be possible to do what I needed to do without permissions to create/modify roles in Azure (which I don’t have), until I took the time to really understand what’s going on in here, and imagine the possibilities. 🙂 I’ll give this link to anyone I encounter in the future who’s in any kind of similar bind. You’re definitely my hero. 🙂 |
|
Micha Wets | February 21, 2020 at 3:54 pm |
Hi Enrika, |