Automate Domain Controller Deployment in Microsoft Azure

Back in summer 2014, I walked you through writing a PowerShell script to deploy domain controllers in Microsoft Azure. In today’s Ask the Admin, I want to revisit the script to address issues including:

  • Problems connecting to the virtual machine (VM) using PowerShell Remoting due to differences in the name of the WinRM endpoint across Azure regions
  • Removing the need to run the script with local administrator privileges
  • Adding the ability to specify a Server Core installation and downgrade the size of the VM accordingly
  • Replacing hardcoded values with defined variables
  • Improving script readability with better formatting

About the Script

Although this version of the script is significantly improved over the original, it’s still a work in progress, and there is undoubtedly room for further optimization. Please take a look at Provision Domain Controllers in Azure using PowerShell on the Petri IT Knowledgebase to get an understanding of how the script works and the prerequisites.

Automating DC deployment in Azure using PowerShell (Image Credit: Russell Smith)
Automating DC deployment in Azure using PowerShell (Image Credit: Russell Smith)

The script uses PowerShell Remoting to connect to the Azure VM so that it can install the Active Directory Domain Services (AD DS) bits, initialize and format the NTDS volume, promote the server to a domain controller, and remove GUI features if a Server Core installation is specified.

The ultimate goal of this script is to replace PowerShell Remoting with Desired State Configuration (DSC), because DSC doesn’t require a connection to be maintained with the remote VM during configuration. The script also works to provide a solution for maintaining the server’s configuration after initial installation. Due to the extra complexity of setting up DSC and the experimental nature of DSC resources available from Microsoft, along with the fact that xAdDomain and xAdDomainController resources have only recently been able to set the AD DatabasePath, LogPath, and SysvolPath values, I’ve decided to stick with PowerShell Remoting for the time being.

New Variables

The custom variables section of the script now contains extra variables to remove hardcoded values, including $domainName, $admin, $siteName, $vmSize, and $osName, all of which should be self-explanatory. I’ve also added $serverCore, which should be set to $true or $false, so that the GUI features can be removed from the default Azure Windows Server image and the VM downgraded to Extra Small at the end of deployment.

​# Set custom variables

$vmName = 'CONTOSODC2'
$serviceName = 'contosodc2'
$ipAddress = '10.0.0.5'
$firstDC = $false
$serverCore = $true
$domainName = 'ad.contoso.com'
$admin = 'contosodc1admin'
$siteName = 'Default-First-Site-Name'
$vmSize = 'Medium'
$osName = 'Windows Server 2012 R2 Datacenter'

PowerShell Remoting Certificate

One frustration with the old script is that if you forget to run it with elevated privileges, then the script fails when trying to add the certificate for PowerShell Remoting to the Local Machine certificate store. Therefore I decided to change the code to use the current user’s certificate store instead:

​$X509Store = New-Object System.Security.Cryptography.X509Certificates.X509Store 'Root', 'CurrentUser'
Certificate warning dialog (Image Credit: Russell Smith)
Certificate warning dialog (Image Credit: Russell Smith)

This isn’t ideal, as Windows prompts you to confirm that it’s okay to add a certificate to the Trusted Root Certification Authorities container, but it’s better than the script throwing an error.

Get the URI for PowerShell Remoting

The previous script enumerated the WinRM port for PowerShell Remoting using the name of the endpoint, which unfortunately isn’t consistent across Azure regions, sometimes causing the script to fail. I recently discovered the Get-AzureWinRMUri cmdlet, which solves this problem by using the cloud service and VM names instead. The –UseSSL parameter has also been removed from the New-PSSession cmdlet, as the $uri string specifies HTTPS.

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

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

Configure Server Core and Extra Small VM Size

I’ve added simple IF statement to remove GUI features from the installation and downgrade to an Extra Small (Basic_A0) VM. Using Server Core in an extra small VM can help keep costs down.

​If ($serverCore -eq $true) {

   Write-Host 'Configuring Server Core and downgrading VM size'

   Invoke-Command -Session $s -ScriptBlock {          
     Get-WindowsFeature *gui* | Uninstall-WindowsFeature         
     }
   Get-AzureVM –ServiceName $serviceName –Name $vmName | Set-AzureVMSize Basic_A0 | Update-AzureVM

}

Disconnect from the Remote Session

And finally a minor detail, code to disconnect from the remote PowerShell session has been added:

​Disconnect-PSSession -Session $s

The Complete Script

As always, you should modify the variables appropriately to suit your own requirements and environment. The script could also be modified to install member servers or meet any other needs you might have.

Set-AzureSubscription –SubscriptionName Pay-As-You-Go -CurrentStorageAccount portalvhdsxgwgzn2ml54p5

# Set custom variables

$vmName = 'CONTOSODC2'
$serviceName = 'contosodc2'
$ipAddress = '10.0.0.5'
$firstDC = $false
$serverCore = $true
$domainName = 'ad.contoso.com'
$admin = 'contosodc1admin'
$siteName = 'Default-First-Site-Name'
$vmSize = 'Medium'
$osName = 'Windows Server 2012 R2 Datacenter'

# Set static variables

$domainadmin = $admin + '@' + $domainName
$compName = $serviceName + '.cloudapp.net'
$passwordsec = convertto-securestring 'PassW0rd!' -asplaintext -force
$password = 'PassW0rd!'
$username = $vmName + 'admin'
$vnetName = 'CONTOSO'
$subNet = 'Subnet-1'
$location = 'North Europe'

# 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
$images = Get-AzureVMImage | where { $_.ImageFamily -eq $osName } | Sort-Object -Descending -Property PublishedDate

# Create a new VM with a static IP address
$newVM = New-AzureVMConfig -Name $vmName -InstanceSize $vmSize -ImageName $images[0].ImageName -DiskLabel 'OS' | Add-AzureProvisioningConfig -Windows -Password $password -AdminUsername $username -DisableAutomaticUpdates | Set-AzureSubnet -SubnetNames $subNet | Set-AzureStaticVNetIP -IPAddress $ipAddress

New-AzureVM -ServiceName $serviceName -VMs $newVM -VNetName $vnetName -Location $location -WaitForBoot

# Attach data disk for AD NTDS files

$myVM = Get-AzureVM -ServiceName $serviceName -Name $vmName
$myVM | Add-AzureDataDisk -CreateNew -DiskSizeInGB 120 -DiskLabel 'NTDS' -LUN 0 | Update-AzureVM

# 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

# Initialize, partition and format disk

Invoke-Command -Session $s -ScriptBlock {

     Get-Disk | where partitionstyle -eq 'raw' | Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem NTFS -NewFileSystemLabel 'NTDS' -Confirm:$false

     # Set AD install paths
     $drive = Get-Volume | where { $_.FileSystemLabel -eq 'NTDS' }
     $NTDSpath = $drive.driveletter + ':\Windows\NTDS'
     $SYSVOLpath = $drive.driveletter + ':\Windows\SYSVOL'

}

# Pass the $password variable to the remote machine, install the AD Directory Services 'bits' and install the first DC in the forest

If ($firstDC -eq $true) {

     Invoke-Command -Session $s -ArgumentList @($passwordsec, $domainName) -ScriptBlock {

          Param (
          $passwordsec, $domainName )

          Write-Host 'Installing the first DC in the domain'
          Install-WindowsFeature –Name AD-Domain-Services -includemanagementtools
          Install-ADDSForest -DatabasePath $NTDSpath -LogPath $NTDSpath -SysvolPath $SYSVOLpath -DomainName $domainName -InstallDns -Force -Confirm:$false -SafeModeAdministratorPassword $passwordsec

     }

}

else

{

     Invoke-Command -Session $s -ArgumentList @($passwordsec, $domainadmin, $domainName, $siteName) -ScriptBlock {

          Param (
          $passwordsec, $domainadmin, $domainName, $siteName )

          # Set domain admin credentials
          $cred = New-Object -typename System.Management.Automation.PSCredential -argumentlist $domainadmin,$passwordsec

          Write-Host 'Installing additional domain controller'
          Install-WindowsFeature –Name AD-Domain-Services -includemanagementtools
          Install-ADDSDomainController -Credential $cred -DatabasePath $NTDSpath -LogPath $NTDSpath -SysvolPath $SYSVOLpath -DomainName $domainName -InstallDns -Force -Confirm:$false -SiteName $siteName -SafeModeAdministratorPassword $passwordsec

     }

}

# Remove GUI features and downgrade VM size

If ($serverCore -eq $true) {

     Write-Host 'Configuring Server Core and downgrading VM size'
     Invoke-Command -Session $s -ScriptBlock {
          Get-WindowsFeature *gui* | Uninstall-WindowsFeature
     }

     Get-AzureVM –ServiceName $serviceName –Name $vmName | Set-AzureVMSize Basic_A0 | Update-AzureVM
}

# Disconnect from PowerShell Remoting session

Disconnect-PSSession -Session $s

# 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