Provision Domain Controllers in Azure using PowerShell

In this Ask an Admin post, we’ll learn how to provision domain controllers in Microsoft Azure using PowerShell with a single click of the mouse.

The cloud is supposed to make provisioning new services easier by providing organizations an ‘elastic’ infrastructure on which IT services can be deployed, managed and extended as needs change. But even simple tasks in Microsoft Azure, such as deploying a domain controller (DC), can be time consuming if undertaken manually with the web management portal and using Remote Desktop to complete the process.

Manually provisioning a new DC usually takes about thirty minutes. This is because the Azure management console is slow and badly designed, static IP addresses must be set separately using PowerShell, and Server Manager on the new DC responds relatively slowly over a remote connection. Add to that the need to check back every few minutes to see if it’s possible to proceed with the next step of the procedure, and you can see that it’s not as straight forward compared to a physical on-premise server.

Although there are some recipes on the web for deploying DCs in Azure using PowerShell or with Desired Configuration Management (DCM), they are often complex and not easy to adapt. I wanted to create a script that was simple and could be used as a means to learn PowerShell, so I set about creating my own solution in which I’m presenting in this article.

Provisioning Domain Controllers in Azure: What the PowerShell Script Does

This PowerShell script can easily be modified for your environment and runs from start to finish without any user interaction, leaving you to concentrate on more important tasks. Here’s a rundown of what the script does:

  1. Creates a new VM from the latest Windows Server 2012 R2 image with a static IP address and additional data disk for the AD NTDS files.
  2. Installs the management certificate on the local PC to enable PowerShell Remoting.
  3. Connects to the VM using PowerShell Remoting and creates a volume for the NTDS files on the data disk.
  4. Again using PowerShell Remoting, installs the Active Directory Domain Services (ADDS) components and promotes the server to a domain controller.
  5. Displays the URL for making a Remote Desktop connection to the new DC.

PowerShell Script Pre-Requisites

Before you can run any of the code included in this article, you will need to have an Azure subscription, install the PowerShell tools for Azure, and set up a secure connection to your subscription as described in “Setup Windows Azure PowerShell Management” on Petri.

You will also need a virtual network configured in Azure with a DNS server set to the IP address of the first domain controller in the domain. See “Setup a Windows Server 2012 R2 Domain Controller in Windows Azure: IP Addressing and Creating a Virtual Network” and “Setup a Windows Server 2012 R2 Domain Controller in Windows Azure: Provision a VM with a Persistent IP Address, Install AD DS” for more information on configuring Active Directory domain controllers in Azure and virtual networking.

Additionally, there are several different considerations:

  • The script assumes that you will deploy each new VM to a unique cloud service.
  • When deploying additional domain controllers to your domain, i.e. when $firstDC is set to $false, all existing domain controllers must be running and contactable.
  • The script has been tested on Windows 8.1 Update 1, so may not work with earlier versions of PowerShell.
  • The script should be run as a local administrator.
  • You have only one root domain in an AD forest.
  • This article assumes that you are familiar with basic Azure and Active Directory concepts, and have some prior scripting experience.

The PowerShell Script Step-By-Step

I’ve kept the script as simple as possible to make it easy to understand and adapt. It contains the absolute minimum logic and error handling, so there’s a lot of room for improvement. I’ll explain the script below by section, so you can adapt it and put it back together again to suit your own environment.

Setting the Scene

The script starts by setting the Azure subscription and storage account. If you’ve done this before on your management workstation, you can omit this from the script in principle. Custom variables are then defined. These are variables that are likely to change every time you run the script, such as the VM name and cloud service name.

$firstDC should to set to $true only when the VM being provisioned will be the first domain controller in the domain. Here we also set the name for the domain administrator ($domainadmin), and it is used only when subsequent DCs join the domain, i.e. when $firstDC is set to $false.

​ Set-AzureSubscription “Pay-As-You-Go” -CurrentStorageAccount portalvhdsxgwgzn2ml54p5

$vmName = "CONTOSODC5"
$serviceName = "contosodc5"
$ipAddress = "10.0.0.8"
$firstDC = $false
$domainadmin = "[email protected]"

Next come static variables, which are values that don’t usually change for each DC deployment. Note that there are two password variables, $passwordsec and $password. Some PowerShell cmdlets require the password in plain text, others in encrypted form. The username for the new VM is automatically generated according to the convention I use in my lab environment, i.e. the VM name + “admin”; hence the domain administrator account ($domainadmin) becomes contosodc1admin.

​ $compName = $serviceName + ".cloudapp.net"
$passwordsec = convertto-securestring "Passw0rd!" -asplaintext -force
$password = "Passw0rd!"
$username = $vmName + "admin"
$vnetName = "CONTOSO"
$subNet = "Subnet-1"
$location = "North Europe"

The script now checks the availability of the cloud service name ($servicename using Test-AzureName) and IP address ($ipAdress using Test-AzureStaticVnetIP) specified in the custom variables section. This is necessary before provisioning a new VM to ensure that the operation completes successfully, otherwise it’s possible to end up with an incorrectly deployed VM, which would then have to be deleted manually.

IP address and cloud service name unavailable error

IP address and cloud service name unavailable error. (Image: Russell Smith)

An error is thrown if the cloud service name or IP address are unavailable. To resolve the issue, change the values in the # Set custom variables section of the script.

​ $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 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"
}

Provision a New Virtual Machine

The next line of code lists the available gallery images for Windows Server 2012 R2 Datacenter edition and puts the latest at the top of the list. In the New-AzureVMConfig command below, we take the image name from the first entry in the $images array ($images[0].ImageName).

​ $images = Get-AzureVMImage | where { $_.ImageFamily -eq “Windows Server 2012 R2 Datacenter” } | Sort-Object -Descending -Property PublishedDate

Now we can specify options for the VM using the New-AzureConfig cmdlet and provision the VM with New-AzureVM.

​ $newVM = New-AzureVMConfig -Name $vmName -InstanceSize “Medium” -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

Finally, we need to add a data disk for the AD NTDS files. This is achieved in two steps to simplify the code, as we will use the $myVM variable continuously throughout the remainder of the script.

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

Configure PowerShell Remoting

Before making a secure connection to the new VM using PowerShell Remoting, the script needs to install the cloud service management certificate on the local computer. The following commands determine the identity of the certificate, and extracts it to a temporary file before creating a new object to import it to the management computer’s local certificate store.

​ $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:TEMPcert.tmp"
$X509Store = New-Object System.Security.Cryptography.X509Certificates.X509Store "Root", "LocalMachine"
$X509Store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$X509Store.Add($X509Object)
$X509Store.Close()

Remove-Item "$env:TEMPcert.tmp"

Once the certificate is installed, we must determine the port for PowerShell Remoting, which is randomly assigned by Azure as VMs are provisioned.

​ $ports = $myVM | Get-AzureEndpoint | where { $_.Name -eq “WinRmHTTPs” }
$powerPort = $ports[0].port

Now a new PowerShell Remoting session can be created with the credentials specified in the variables section at the start of the script.

​ $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username,$passwordsec
$s = New-PSSession -ComputerName $compName -port $powerPort -Credential $cred -UseSSL

Create and Format a New Volume

Before promoting the server to a domain controller, the data disk needs to be prepared for the AD NTDS logs. The Get-Disk cmdlet is used to initialize the disk we added to the VM, as well as to create and format a new volume. See how the Invoke-Command cmdlet is used to run the Get-Disk command on the remote VM.

The Get-Volume cmdlet is used to determine the drive letter assigned to the volume that the script created in the last section. This cmdlet also creates the file paths to pass on to the Install-ADDSForest and Install-ADDSDomainController cmdlets, respectively.

​ 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 + ":WindowsNTDS"
$SYSVOLpath = $drive.driveletter + ":WindowsSYSVOL"
}

Promote the Server to a Domain Controller

This section of the script contains some logic based on the status of the $firstDC variable. This is because the commands for promoting a server to a DC are different, depending on whether this is the first DC in an Active Directory domain or forest. You can see in the code below the different commands used.

Both sections of code use Install-WindowsFeature to get the AD Domain Services bits on to the server, and pass one or more variables defined in the script on the local computer ($passwordsec and $domainadmin) to the remote computer using –ArgumentList and Param.

The script completes displaying the URL for connecting via Remote Desktop

The script completes displaying the URL for connecting via Remote Desktop. (Image: Russell Smith)

When $firstDC is set to $false, in addition to what happens for the first DC in a domain, the script securely passes the credentials for the domain administrator account ($domainadmin) to the Install-ADDSDomainController cmdlet.

​ If ($firstDC -eq $true) { 

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

Param (
$passwordsec )

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 "ad.contoso.com" -InstallDns -Force -Confirm:$false -SafeModeAdministratorPassword $passwordsec

}

}

else 

{

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

Param (
$passwordsec, $domainadmin )

# 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 "ad.contoso.com" -InstallDns -Force -Confirm:$false -SiteName "Default-First-Site-Name" -SafeModeAdministratorPassword $passwordsec

}

}

Connecting using Remote Desktop

Finally, the script displays the URL for connecting to the new VM using Remote Desktop, determing the RDP port in a similar way to which it did for the PowerShell Remoting port.

​ $rdpPort = $myVM | Get-AzureEndpoint | where { $_.Name -eq “RDP” }
$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

The Complete Script

Naturally there are some aspects of the script that are not optimal, such as the use of hardcoded passwords in plaintext, but for a lab environment, that’s not likely to be a problem. You can download a commented version of the entire script here:

I suggest that you modify it for your environment using the PowerShell Integrated Scripting Environment (ISE).