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