Automate Domain Member Server Deployment in Microsoft Azure

In a previous Ask the Admin, I outlined some improvements to my PowerShell script to Automate Domain Controller Deployment in Microsoft Azure on the Petri IT Knowledgebase. But today, it’s time to turn to member servers, and I’ll walk you through the improvements made since the original article, Using PowerShell to Provision Member Servers in Microsoft Azure, was published.

Many of the improvements I made to the domain controller (DC) script have been rolled forward into the member server script where it makes sense. For example implementing the Get-AzureWinRMUri cmdlet to determine the Windows Remote Management (WinRM) port, using the user certificate store instead of the local computer store to install the certificate for PowerShell Remoting, and disconnecting from the remote PowerShell session at the end of the script. Currently the member server script doesn’t have code to specify a Server Core installation and downgrade the size of the VM because it’s not a requirement in my dev environment.

Deploy a domain member server in Azure using PowerShell (Image Credit: Russell Smith)
Deploy a domain member server in Azure using PowerShell (Image Credit: Russell Smith)

Persistent IP Address

The original script didn’t configure the VM with a persistent internal IP address, and it’s certainly not an absolute requirement. But remember that while it’s possible to configure VMs with a persistent IP address, it’s not the same as a DHCP reservation. As such, there are some instances where IP addressing can be problematic if all servers are not configured with a persistent IP address, so the code I used to assign DCs a persistent IP address has been added to the member server script.

Consider a situation where a member server that isn’t configured with a persistent IP address is started while all other servers, including domain controllers, are stopped. In this scenario, the member server will be assigned the first available address in the pool, meaning that when you later start a DC, the IP address that it has been configured to request may not be available for use. But if all servers are configured to use a persistent IP address, VMs can be started in any order without the risk of address conflicts. In production environments, where VMs are usually left running continuously, the issue I described above may never arise.

Affinity Groups

In both the DC and member server scripts, I have swapped out the –Location parameter for –AffinityGroup in the New-AzureVM cmdlet, and added the $affinityGroup variable. In a recent drive to reduce costs and improve performance, I created an affinity group in my Azure subscription to make sure that storage and VMs are all located on the same cluster in the datacenter. Affinity groups are not compulsory when creating storage accounts or provisioning VMs, but they do help to ensure that resources are located close to each other for best performance and low costs, and not at opposite ends of a datacenter.

​
For more information on creating affinity groups and associating a storage account with an affinity group, see Optimizing Azure Storage for Windows Server Virtual Machines on Petri.


Enable WinRM HTTP Listener

The default HTTP WinRM listener is missing from Microsoft Azure Windows Server 2012 R2 images in the gallery and causes problems when connecting remotely to servers using management tools, such as Server Manager. A missing default HTTP WinRM listener can also create other issues, so I decided to add the ability to reinstate the default server configuration as part of the script. PowerShell remoting is used to run the winrm command on the server.

Disable Remote Desktop NLA

Finally, I added the option to disable Remote Desktop Network Level Authentication (NLA), to make sure that I can always log in to member servers, regardless of the order in which VMs are started in the dev environment, and if there’s no DC available for authentication. The code below uses Windows Management Instrumentation (WMI) and PowerShell remoting to disable NLA.
​
See the full script with modifications below.
​
# Set custom variables

$vmName = 'Contososrv2'
$serviceName = 'Contososrv22'
$ipAddress = '10.0.0.6'

# Set static variables

$domainadmin = 'contosodc1admin'
$password = 'PassW0rd!'
$passwordsec = convertto-securestring $password -asplaintext -force
$username = $vmName + 'admin'
$vnetName = 'CONTOSO'
$subNet = 'Subnet-1'
$affinityGroup = 'Contoso'
$domain = 'ad.contoso.com'
$netBios = 'AD'
$imageFamily = 'Windows Server 2012 R2 Datacenter'
$instanceSize = 'Small'
$winRM = $true
$disableNLA = $true

# Check availability of IP address and cloud service name

$IPtest = Test-AzureStaticVnetIP -VNetName $vnetName -IPAddress $ipAddress
$cservices = Test-AzureName -service -name $serviceName

If(($cservices -eq $true) -or ($IPtest.IsAvailable -eq $false)) {
    
    If ($cservices -eq $true) {
        Write-Host 'The cloud service name already exists' -foregroundcolor yellow -backgroundcolor red }

    If ($IPtest.IsAvailable -eq $false) {
        Write-Host 'The IP address is not available' -foregroundcolor yellow -backgroundcolor red }
        throw 'An error occurred'
}

# Get the name of the latest image
$imageName = Get-AzureVMImage | where { $_.ImageFamily -eq $imageFamily } | sort PublishedDate -Descending | select -ExpandProperty ImageName -First 1

# Create a new VM

$newVM = New-AzureVMConfig -Name $vmName -InstanceSize $instanceSize -ImageName $imageName -DiskLabel 'OS'
$newVM | Add-AzureProvisioningConfig -WindowsDomain -AdminUsername $username -Password $password -DomainUserName $domainadmin -DomainPassword $password -Domain $netBios -JoinDomain $domain -DisableAutomaticUpdates
$newVM | Set-AzureSubnet -SubnetNames $subNet
$newVM | Set-AzureStaticVNetIP -IPAddress $ipAddress

New-AzureVM -ServiceName $serviceName -VMs $newVM -VNetName $vnetName -AffinityGroup $affinityGroup -WaitForBoot

$myVM = Get-AzureVM -ServiceName $serviceName -Name $vmName

# Install the cert for the VM locally

$WinRMCertificateThumbprint = ($myVM | Select-Object -ExpandProperty VM).DefaultWinRMCertificateThumbprint
(Get-AzureCertificate -ServiceName $serviceName -Thumbprint $WinRMCertificateThumbprint -ThumbprintAlgorithm SHA1).Data | Out-File "${env:TEMP}\cert.tmp"
 
$X509Object = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 "$env:TEMP\cert.tmp"
$X509Store = New-Object System.Security.Cryptography.X509Certificates.X509Store 'Root', 'CurrentUser'
$X509Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$X509Store.Add($X509Object)
$X509Store.Close()
 
Remove-Item "$env:TEMP\cert.tmp"

# Get the URI for PowerShell Remoting

$uri = Get-AzureWinRMUri –Service $serviceName –Name $vmName

# Create new PowerShell Remoting session

$cred = New-Object -typename System.Management.Automation.PSCredential -argumentlist $username,$passwordsec

$s = New-PSSession –ConnectionUri $uri -Credential $cred

# Enable WinRM

If ($winRM -eq $true) { 

    Write-Host 'Enabling WinRM'
    Invoke-Command -Session $s -ScriptBlock {
        winrm quickconfig -force
        }
}

# Disable Remote Desktop NLA

If ($disableNLA -eq $true) { 

    Write-Host 'Disabling Remote Desktop NLA'
    Invoke-Command -Session $s -ArgumentList @($vmName) -ScriptBlock {
        Param ($vmName)
        (Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -ComputerName $vmName -Filter "TerminalName='RDP-tcp'").SetUserAuthenticationRequired(0)
        }
}

# Display the RDP connection string
$rdpPort = $myVM | Get-AzureEndpoint | where { $_.LocalPort -eq '3389' }
$rdpString = $servicename + '.cloudapp.net:' + $rdpPort.Port
write-host 'Make a Remote Desktop connection to the VM using the URL below:' -foregroundcolor yellow -backgroundcolor red 
write-host $rdpString

# Disconnect from PowerShell Remoting session

Disconnect-PSSession -Session $s