So, I have a new project coming up where I will be required to manage, maintain, and support an RDS Deployment for a local engineering firm.
And after working with some of the brightest PowerShell experts in the world on the Master PowerShell Tricks series I decided to cut ties to the GUI and build it 100 % using PowerShell.
The requirements for me to test this are actually a bit complicated because I wanted to have a test lab to play with.
Luckily, I had already build my BigDemo PowerShell Script that included all the functions I would need to get started.
Let’s commence the work at around 3:00 PM I started modifying the code in my existing script.
If you recall I use this same script to build out my Storage Spaces Direct Farms.
Now I have a couple of functions that I use to build the base VM’s from the Base Virtual Disks and then do their post configurations.
function Invoke-DemoVMPrep { param ( [string] $VMName, [string] $GuestOSName, [switch] $FullServer ) Write-Log $VMName 'Removing old VM' get-vm $VMName -ErrorAction SilentlyContinue | stop-vm -TurnOff -Force -Passthru | remove-vm -Force Clear-File "$($VMPath)\$($GuestOSName).vhdx" Write-Log $VMName 'Creating new differencing disk' if ($FullServer) { $null = New-VHD -Path "$($VMPath)\$($GuestOSName).vhdx" -ParentPath "$($BaseVHDPath)\VMServerBase.vhdx" -Differencing } else { $null = New-VHD -Path "$($VMPath)\$($GuestOSName).vhdx" -ParentPath "$($BaseVHDPath)\VMServerBaseCore.vhdx" -Differencing } Write-Log $VMName 'Creating virtual machine' new-vm -Name $VMName -MemoryStartupBytes 16GB -SwitchName $virtualSwitchName ` -Generation 2 -Path "$($VMPath)\" | Set-VM -ProcessorCount 2 Set-VMFirmware -VMName $VMName -SecureBootTemplate MicrosoftUEFICertificateAuthority Set-VMFirmware -Vmname $VMName -EnableSecureBoot off Add-VMHardDiskDrive -VMName $VMName -Path "$($VMPath)\$($GuestOSName).vhdx" -ControllerType SCSI Write-Log $VMName 'Starting virtual machine' Enable-VMIntegrationService -Name 'Guest Service Interface' -VMName $VMName start-vm $VMName } function Create-DemoVM { param ( [string] $VMName, [string] $GuestOSName, [string] $IPNumber = '0' ) Wait-PSDirect $VMName -cred $localCred Invoke-Command -VMName $VMName -Credential $localCred { param($IPNumber, $GuestOSName, $VMName, $domainName, $Subnet) if ($IPNumber -ne '0') { Write-Output -InputObject "[$($VMName)]:: Setting IP Address to $($Subnet)$($IPNumber)" $null = New-NetIPAddress -IPAddress "$($Subnet)$($IPNumber)" -InterfaceAlias 'Ethernet' -PrefixLength 24 Write-Output -InputObject "[$($VMName)]:: Setting DNS Address" Get-DnsClientServerAddress | ForEach-Object -Process { Set-DnsClientServerAddress -InterfaceIndex $_.InterfaceIndex -ServerAddresses "$($Subnet)1" } } Write-Output -InputObject "[$($VMName)]:: Renaming OS to `"$($GuestOSName)`"" Rename-Computer -NewName $GuestOSName Write-Output -InputObject "[$($VMName)]:: Configuring WSMAN Trusted hosts" Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value "*.$($domainName)" -Force Set-Item WSMan:\localhost\client\trustedhosts "$($Subnet)*" -Force -concatenate Enable-WSManCredSSP -Role Client -DelegateComputer "*.$($domainName)" -Force } -ArgumentList $IPNumber, $GuestOSName, $VMName, $domainName, $Subnet Restart-DemoVM $VMName Wait-PSDirect $VMName -cred $localCred }
After the Servers are build using Invoke-DemoVMPrep we use the Create-DemoVM to do their final configs… here is what it looks like inside the script.
Now in this example I build a Domain Controller, MGMT Server, and DHCP Server, and the basic VM’s build for the RDS Farm.
Invoke-DemoVMPrep 'DHCP1-RDS' 'DHCP1-RDS' -FullServer Invoke-DemoVMPrep 'MGMT1-RDS' 'MGMT1-RDS' -FullServer Invoke-DemoVMPrep 'RDSH01-RDS' 'RDSH01-RDS' -FullServer Invoke-DemoVMPrep 'RDSH02-RDS' 'RDSH02-RDS' -FullServer Invoke-DemoVMPrep 'RDGW01-RDS' 'RDGW01-RDS' -FullServer Invoke-DemoVMPrep 'RDAPP01-RDS' 'RDAPP01-RDS' -FullServer Invoke-DemoVMPrep 'DC1-RDS' 'DC1-RDS' -FullServer $VMName = 'DC1-RDS' $GuestOSName = 'DC1-RDS' $IPNumber = '1' Create-DemoVM $VMName $GuestOSName $IPNumber Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainName, $domainAdminPassword) Write-Output -InputObject "[$($VMName)]:: Installing AD" $null = Install-WindowsFeature AD-Domain-Services -IncludeManagementTools Write-Output -InputObject "[$($VMName)]:: Enabling Active Directory and promoting to domain controller" Install-ADDSForest -DomainName $domainName -InstallDNS -NoDNSonNetwork -NoRebootOnCompletion ` -SafeModeAdministratorPassword (ConvertTo-SecureString -String $domainAdminPassword -AsPlainText -Force) -confirm:$false } -ArgumentList $VMName, $domainName, $domainAdminPassword Restart-DemoVM $VMName $VMName = 'DHCP1-RDS' $GuestOSName = 'DHCP1-RDS' $IPNumber = '3' Create-DemoVM $VMName $GuestOSName $IPNumber Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainCred, $domainName) Write-Output -InputObject "[$($VMName)]:: Installing DHCP" $null = Install-WindowsFeature DHCP -IncludeManagementTools Write-Output -InputObject "[$($VMName)]:: Joining domain as `"$($env:computername)`"" while (!(Test-Connection -ComputerName $domainName -BufferSize 16 -Count 1 -Quiet -ea SilentlyContinue)) { Start-Sleep -Seconds 1 } do { Add-Computer -DomainName $domainName -Credential $domainCred -ea SilentlyContinue } until ($?) } -ArgumentList $VMName, $domainCred, $domainName Restart-DemoVM $VMName Wait-PSDirect $VMName -cred $domainCred Invoke-Command -VMName $VMName -Credential $domainCred { param($VMName, $domainName, $Subnet, $IPNumber) Write-Output -InputObject "[$($VMName)]:: Waiting for name resolution" while ((Test-NetConnection -ComputerName $domainName).PingSucceeded -eq $false) { Start-Sleep -Seconds 1 } Write-Output -InputObject "[$($VMName)]:: Configuring DHCP Server" Set-DhcpServerv4Binding -BindingState $true -InterfaceAlias Ethernet Add-DhcpServerv4Scope -Name 'IPv4 Network' -StartRange "$($Subnet)10" -EndRange "$($Subnet)200" -SubnetMask 255.255.255.0 Set-DhcpServerv4OptionValue -OptionId 6 -value "$($Subnet)1" Add-DhcpServerInDC -DnsName "$($env:computername).$($domainName)" foreach($i in 1..99) { $mac = '00-b5-5d-fe-f6-' + ($i % 100).ToString('00') $ip = $Subnet + '1' + ($i % 100).ToString('00') $desc = 'Container ' + $i.ToString() $scopeID = $Subnet + '0' Add-DhcpServerv4Reservation -IPAddress $ip -ClientId $mac -Description $desc -ScopeId $scopeID } } -ArgumentList $VMName, $domainName, $Subnet, $IPNumber Restart-DemoVM $VMName
Now that I had my configurations started I finished up by running Create-DemoVM on the RDS Farm instances which basically just joined them to the domain and restarted them.
$VMName = 'MGMT1-RDS' $GuestOSName = 'MGMT1-RDS' Create-DemoVM $VMName $GuestOSName Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainCred, $domainName) Write-Output -InputObject "[$($VMName)]:: Management tools" $null = Install-WindowsFeature RSAT-Clustering, RSAT-Hyper-V-Tools Write-Output -InputObject "[$($VMName)]:: Joining domain as `"$($env:computername)`"" while (!(Test-Connection -ComputerName $domainName -BufferSize 16 -Count 1 -Quiet -ea SilentlyContinue)) { Start-Sleep -Seconds 1 } do { Add-Computer -DomainName $domainName -Credential $domainCred -ea SilentlyContinue } until ($?) } -ArgumentList $VMName, $domainCred, $domainName Restart-DemoVM $VMName $VMName = 'RDSH01-RDS' $GuestOSName = 'RDSH01-RDS' Create-DemoVM $VMName $GuestOSName Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainCred, $domainName) Write-Output -InputObject "[$($VMName)]:: Management tools" # $null = Install-WindowsFeature RSAT-Clustering, RSAT-Hyper-V-Tools Write-Output -InputObject "[$($VMName)]:: Joining domain as `"$($env:computername)`"" while (!(Test-Connection -ComputerName $domainName -BufferSize 16 -Count 1 -Quiet -ea SilentlyContinue)) { Start-Sleep -Seconds 1 } do { Add-Computer -DomainName $domainName -Credential $domainCred -ea SilentlyContinue } until ($?) } -ArgumentList $VMName, $domainCred, $domainName Restart-DemoVM $VMName $VMName = 'RDSH02-RDS' $GuestOSName = 'RDSH02-RDS' Create-DemoVM $VMName $GuestOSName Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainCred, $domainName) Write-Output -InputObject "[$($VMName)]:: Management tools" #$null = Install-WindowsFeature RSAT-Clustering, RSAT-Hyper-V-Tools Write-Output -InputObject "[$($VMName)]:: Joining domain as `"$($env:computername)`"" while (!(Test-Connection -ComputerName $domainName -BufferSize 16 -Count 1 -Quiet -ea SilentlyContinue)) { Start-Sleep -Seconds 1 } do { Add-Computer -DomainName $domainName -Credential $domainCred -ea SilentlyContinue } until ($?) } -ArgumentList $VMName, $domainCred, $domainName Restart-DemoVM $VMName $VMName = 'RDGW01-RDS' $GuestOSName = 'RDGW01-RDS' Create-DemoVM $VMName $GuestOSName Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainCred, $domainName) Write-Output -InputObject "[$($VMName)]:: Management tools" #$null = Install-WindowsFeature RSAT-Clustering, RSAT-Hyper-V-Tools Write-Output -InputObject "[$($VMName)]:: Joining domain as `"$($env:computername)`"" while (!(Test-Connection -ComputerName $domainName -BufferSize 16 -Count 1 -Quiet -ea SilentlyContinue)) { Start-Sleep -Seconds 1 } do { Add-Computer -DomainName $domainName -Credential $domainCred -ea SilentlyContinue } until ($?) } -ArgumentList $VMName, $domainCred, $domainName Restart-DemoVM $VMName $VMName = 'RDAPP01-RDS' $GuestOSName = 'RDAPP01-RDS' Create-DemoVM $VMName $GuestOSName Invoke-Command -VMName $VMName -Credential $localCred { param($VMName, $domainCred, $domainName) Write-Output -InputObject "[$($VMName)]:: Management tools" $null = Install-WindowsFeature RSAT-Clustering, RSAT-Hyper-V-Tools Write-Output -InputObject "[$($VMName)]:: Joining domain as `"$($env:computername)`"" while (!(Test-Connection -ComputerName $domainName -BufferSize 16 -Count 1 -Quiet -ea SilentlyContinue)) { Start-Sleep -Seconds 1 } do { Add-Computer -DomainName $domainName -Credential $domainCred -ea SilentlyContinue } until ($?) } -ArgumentList $VMName, $domainCred, $domainName Restart-DemoVM $VMName
The coolest part about all of this is that I am running all of this infrastructure on my 2-node Storage Spaces Direct All Flash Array and it only took 20 minutes to build this start to finish.
Here is the Script building finished product looked like this: This final run was done at around 4:14 PM
Here are the VM’s Built in Hyper-V
Now the coolest part of what I wanted to do was to automate the build of the RDS Farm with PowerShell DSC.
To accomplish this I used a PSGallery Item called xRemoteDesktopSessionHost v.1.4.0.0 which can be found here:
https://www.powershellgallery.com/packages/xRemoteDesktopSessionHost/1.4.0.0
Now with the help of Will Anderson one of the amazing Honorary Scripting Guys at Microsoft I was able to install this DSCResource without having to do much other than execute this one line of PowerShell on my
target machine:
Find-Module xRemoteDesktopSessionHost | Install-Module
Once done I had the PowerShell DSC module that would be required for me to proceed.
For tonight’s testing, I decided to do a single server configuration to see how hard it would be.
Here is the DSC Configuration I used to build out my base configuration for testing:
param ( [string]$brokerFQDN, [string]$webFQDN, [string]$collectionName, [string]$collectionDescription ) $localhost = [System.Net.Dns]::GetHostByName((hostname)).HostName if (!$collectionName) {$collectionName = "DK Collection"} if (!$collectionDescription) {$collectionDescription = "Remote Desktop instance for accessing an isolated network environment."} Configuration RemoteDesktopSessionHost { param ( # Connection Broker Name [Parameter(Mandatory)] [String]$collectionName, # Connection Broker Description [Parameter(Mandatory)] [String]$collectionDescription, # Connection Broker Node Name [String]$connectionBroker, # Web Access Node Name [String]$webAccessServer ) Import-DscResource -Module xRemoteDesktopSessionHost if (!$connectionBroker) {$connectionBroker = $localhost} if (!$connectionWebAccessServer) {$webAccessServer = $localhost} Node "localhost" { LocalConfigurationManager { RebootNodeIfNeeded = $true } WindowsFeature Remote-Desktop-Services { Ensure = "Present" Name = "Remote-Desktop-Services" } WindowsFeature RDS-RD-Server { Ensure = "Present" Name = "RDS-RD-Server" } WindowsFeature Desktop-Experience { Ensure = "Present" Name = "Desktop-Experience" } WindowsFeature RSAT-RDS-Tools { Ensure = "Present" Name = "RSAT-RDS-Tools" IncludeAllSubFeature = $true } if ($localhost -eq $connectionBroker) { WindowsFeature RDS-Connection-Broker { Ensure = "Present" Name = "RDS-Connection-Broker" } } if ($localhost -eq $webAccessServer) { WindowsFeature RDS-Web-Access { Ensure = "Present" Name = "RDS-Web-Access" } } WindowsFeature RDS-Licensing { Ensure = "Present" Name = "RDS-Licensing" } xRDSessionDeployment Deployment { SessionHost = $localhost ConnectionBroker = if ($ConnectionBroker) {$ConnectionBroker} else {$localhost} WebAccessServer = if ($WebAccessServer) {$WebAccessServer} else {$localhost} DependsOn = "[WindowsFeature]Remote-Desktop-Services", "[WindowsFeature]RDS-RD-Server" } xRDSessionCollection Collection { CollectionName = $collectionName CollectionDescription = $collectionDescription SessionHost = $localhost ConnectionBroker = if ($ConnectionBroker) {$ConnectionBroker} else {$localhost} DependsOn = "[xRDSessionDeployment]Deployment" } xRDSessionCollectionConfiguration CollectionConfiguration { CollectionName = $collectionName CollectionDescription = $collectionDescription ConnectionBroker = if ($ConnectionBroker) {$ConnectionBroker} else {$localhost} TemporaryFoldersDeletedOnExit = $false SecurityLayer = "SSL" DependsOn = "[xRDSessionCollection]Collection" } xRDRemoteApp Calc { CollectionName = $collectionName DisplayName = "Calculator" FilePath = "C:\Windows\System32\calc.exe" Alias = "calc" DependsOn = "[xRDSessionCollection]Collection" } xRDRemoteApp Mstsc { CollectionName = $collectionName DisplayName = "Remote Desktop" FilePath = "C:\Windows\System32\mstsc.exe" Alias = "mstsc" DependsOn = "[xRDSessionCollection]Collection" } xRDRemoteApp WordPad { CollectionName = $collectionName DisplayName = "WordPad" FilePath = "C:\Program Files\Windows NT\Accessories\wordpad.exe" Alias = "wordpad" DependsOn = "[xRDSessionCollection]Collection" } xRDRemoteApp CMD { CollectionName = $collectionName DisplayName = "CMD" FilePath = "C:\windows\system32\cmd.exe" Alias = "cmd" DependsOn = "[xRDSessionCollection]Collection" } } } write-verbose "Creating configuration with parameter values:" write-verbose "Collection Name: $collectionName" write-verbose "Collection Description: $collectionDescription" write-verbose "Connection Broker: $brokerFQDN" write-verbose "Web Access Server: $webFQDN" RemoteDesktopSessionHost -collectionName $collectionName -collectionDescription $collectionDescription -connectionBroker $brokerFQDN -webAccessServer $webFQDN -OutputPath .\RDSDSC\ Set-DscLocalConfigurationManager -verbose -path .\RDSDSC\ Start-DscConfiguration -wait -force -verbose -path .\RDSDSC\
Here was a snip of the script in action building the single node RDS Test Server:
Here was a screenshot of the completely installed farm.
Here is a Screenshot of the view from the client’s perspective
I did do some testing by removing some of the applications and then re-running the DSC Configuration and as expected the just got re-published.
I really hope you enjoyed this and I promise I will provide more updates on the progress for the rest of the farm as I get it done.
Thanks,
Dave
Trackbacks/Pingbacks