Last Update: Sep 04, 2024 | Published: Mar 10, 2015
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:
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.
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.
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'
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'
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.
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
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 }
And finally a minor detail, code to disconnect from the remote PowerShell session has been added:
Disconnect-PSSession -Session $s
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:TEMPcert.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:TEMPcert.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 + ':WindowsNTDS' $SYSVOLpath = $drive.driveletter + ':WindowsSYSVOL' } # 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