I keep creating and destroying my virtual lab in Azure, I figure it’s time to script it so I can easily copy paste and have a template in hand. Previously I was doing some parts via the Web UI, some parts via PowerShell.
These are mostly notes to myself, keep that in mind as you go through them …
Also, I am writing these on the small screen of my Notion Ink Cain laptop/ tablet so parts of it are not as elegant as I’d like them to be. My initial plan was to write a script that would setup a lab and some DCs and servers. Now the plan is to write that in a later post (once I get this adapter I’ve ordered to connect the Cain to my regular monitor). What follows are the overall steps and cmdlets, not a concise scripted version.
Step 1: Create an affinity group
This would be a one time thing (unless you delete the affinity group too when starting afresh or want a new one). I want to create one in SouthEast Asia as that’s closest for me.
1 2 3 4 5 |
PS> New-AzureAffinityGroup -Name "SouthEastAsia" -Description "SouthEast Asia affinity group" -Location "SouthEast Asia" OperationDescription OperationId OperationStatus -------------------- ----------- --------------- New-AzureAffinityGroup 5b2e4d08-bd01-6913-9257-09d6c313bb87 Succeeded |
Note: the name cannot contain any spaces. And if you are curious about affinity groups check out this TechNet post.
Step 2: Set up the network
Let’s start with a configuration file like this. It defines three sites – London, Muscat, Dubai – with separate address spaces. Note that I am using address spaces – this means the three sites will not be able to talk to each other until I set up site-to-site connectivity between them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<?xml version="1.0" encoding="utf-8"?> <NetworkConfiguration xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/ServiceHosting/2011/07/NetworkConfiguration"> <VirtualNetworkConfiguration> <!-- <Dns> <DnsServers> <DnsServer name="LATER" IPAddress="LATER" /> <DnsServer name="LATER" IPAddress="LATER" /> <DnsServer name="LATER" IPAddress="LATER" />/> <DnsServer name="LATER" IPAddress="LATER" /> </DnsServers> </Dns> --> <VirtualNetworkSites> <VirtualNetworkSite name="LONDON" AffinityGroup="SouthEastAsia"> <AddressSpace> <AddressPrefix>192.168.10.0/24</AddressPrefix> </AddressSpace> <Subnets> <Subnet name="Servers"> <AddressPrefix>192.168.10.0/25</AddressPrefix> </Subnet> <Subnet name="Clients"> <AddressPrefix>192.168.10.128/25</AddressPrefix> </Subnet> </Subnets> </VirtualNetworkSite> <VirtualNetworkSite name="MUSCAT" AffinityGroup="SouthEastAsia"> <AddressSpace> <AddressPrefix>192.168.50.0/24</AddressPrefix> </AddressSpace> <Subnets> <Subnet name="Servers"> <AddressPrefix>192.168.50.0/25</AddressPrefix> </Subnet> <Subnet name="Clients"> <AddressPrefix>192.168.50.128/25</AddressPrefix> </Subnet> </Subnets> </VirtualNetworkSite> <VirtualNetworkSite name="DUBAI" AffinityGroup="SouthEastAsia"> <AddressSpace> <AddressPrefix>192.168.25.0/24</AddressPrefix> </AddressSpace> <Subnets> <Subnet name="Servers"> <AddressPrefix>192.168.25.0/25</AddressPrefix> </Subnet> <Subnet name="Clients"> <AddressPrefix>192.168.25.128/25</AddressPrefix> </Subnet> </Subnets> </VirtualNetworkSite> </VirtualNetworkSites> </VirtualNetworkConfiguration> </NetworkConfiguration> |
Within each address space I also create two subnets – one for the servers, another for clients. Not strictly needed, I just like to keep them separate.
Note that all three networks are in the same affinity group. Again, it doesn’t matter, but since this is a test lab I’d like for them to be together.
Save this XML file someplace and push it to Azure:
1 2 3 4 5 |
PS> Set-AzureVNetConfig -ConfigurationPath C:\Users\Rakhesh\Downloads\AzureVNet.xml OperationDescription OperationId OperationStatus -------------------- ----------- --------------- Set-AzureVNetConfig 4f088382-fa8d-6774-a3b3-1b08e61e2ab2 Succeeded |
That’s it!
Step 3: Create a storage account
1 2 3 4 5 |
PS> New-AzureStorageAccount -StorageAccountName "rakheshlocallyredundant" -Description "Locally redundant storage. SouthEast Asia" -AffinityGroup "SouthEastAsia" -Type Standard_LRS OperationDescription OperationId OperationStatus -------------------- ----------- --------------- New-AzureStorageAccount b8915a48-c638-67bf-9950-16dae07fd26e Succeeded |
Note: the name has to be unique in Azure and must be all small letters. A locally redundant storage account is sufficient for test lab purposes.
Associate this storage account with my subscription.
1 |
PS> Set-AzureSubscription -CurrentStorageAccountName rakheshlocallyredundant -SubscriptionName "Visual Studio Ultimate with MSDN" |
That’s it!
Step 4: Create a VM
This is similar to an earlier post but slightly different.
Get a list of Server 2012 images:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
PS> Get-AzureVMImage | ?{ $_.Label -match "^Windows Server 2012" } | fl ImageName,Label ImageName : a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-Datacenter-201410.01-en.us-127GB.vhd Label : Windows Server 2012 Datacenter, October 2014 ImageName : a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-Datacenter-201411.01-en.us-127GB.vhd Label : Windows Server 2012 Datacenter, November 2014 ImageName : a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-Datacenter-201412.01-en.us-127GB.vhd Label : Windows Server 2012 Datacenter, December 2014 ImageName : a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201410.01-en.us-127GB.vhd Label : Windows Server 2012 R2 Datacenter, October 2014 ImageName : a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201411.01-en.us-127GB.vhd Label : Windows Server 2012 R2 Datacenter, November 2014 ImageName : a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201412.01-en.us-127GB.vhd Label : Windows Server 2012 R2 Datacenter, December 2014 |
The last one is what I want. I create a variable to which I add the provisioning info for this new server. For convenience I use the server name as the variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
PS> $LONSDC01 = New-AzureVMConfig -Name "LONSDC01" -InstanceSize Basic_A1 -ImageName "a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201412.01-en.us-127GB.vhd" PS> $LONSDC01 | Add-AzureProvisioningConfig -Windows -AdminUsername "LocalUser" -Password "Password in Plaintext" -TimeZone "Arabian Standard Time" AvailabilitySetName : ConfigurationSets : {LONSDC01, Microsoft.WindowsAzure.Commands.ServiceManagement.Model.NetworkConfigurationSet} DataVirtualHardDisks : {} Label : LONSDC01 OSVirtualHardDisk : Microsoft.WindowsAzure.Commands.ServiceManagement.Model.OSVirtualHardDisk RoleName : LONSDC01 RoleSize : Basic_A1 RoleType : PersistentVMRole WinRMCertificate : X509Certificates : {} NoExportPrivateKey : False NoRDPEndpoint : False NoSSHEndpoint : False DefaultWinRmCertificateThumbprint : ProvisionGuestAgent : True ResourceExtensionReferences : {BGInfo} DataVirtualHardDisksToBeDeleted : |
Assign this server to a subnet and set a static IP address (the latter is optional; also, remember the first 3 IP addresses in a subnet are reserved by Azure).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
PS> $LONSDC01 | Set-AzureSubnet -SubnetNames "Servers" AvailabilitySetName : ConfigurationSets : {LONSDC01, Microsoft.WindowsAzure.Commands.ServiceManagement.Model.NetworkConfigurationSet} DataVirtualHardDisks : {} Label : LONSDC01 OSVirtualHardDisk : Microsoft.WindowsAzure.Commands.ServiceManagement.Model.OSVirtualHardDisk RoleName : LONSDC01 RoleSize : Basic_A1 RoleType : PersistentVMRole WinRMCertificate : X509Certificates : {} NoExportPrivateKey : False NoRDPEndpoint : False NoSSHEndpoint : False DefaultWinRmCertificateThumbprint : ProvisionGuestAgent : True ResourceExtensionReferences : {BGInfo} DataVirtualHardDisksToBeDeleted : PS> $LONSDC01 | Set-AzureStaticVNetIP -IPAddress "192.168.10.4" AvailabilitySetName : ConfigurationSets : {LONSDC01, Microsoft.WindowsAzure.Commands.ServiceManagement.Model.NetworkConfigurationSet} DataVirtualHardDisks : {} Label : LONSDC01 OSVirtualHardDisk : Microsoft.WindowsAzure.Commands.ServiceManagement.Model.OSVirtualHardDisk RoleName : LONSDC01 RoleSize : Basic_A1 RoleType : PersistentVMRole WinRMCertificate : X509Certificates : {} NoExportPrivateKey : False NoRDPEndpoint : False NoSSHEndpoint : False DefaultWinRmCertificateThumbprint : ProvisionGuestAgent : True ResourceExtensionReferences : {BGInfo} DataVirtualHardDisksToBeDeleted : |
Next, create a cloud service associated with this VM, and create a new VM associating it with this service and a Virtual Network.
1 2 3 4 5 6 7 8 9 10 11 |
PS> New-AzureService -ServiceName "LONSDC01" -AffinityGroup "SouthEastAsia" OperationDescription OperationId OperationStatus -------------------- ----------- --------------- New-AzureService e381f27c-fe67-65a2-9036-626567a09ecf Succeeded PS> $LONSDC01 | New-AzureVM -ServiceName "LONSDC01" -VNetName "LONDON" OperationDescription OperationId OperationStatus -------------------- ----------- --------------- New-AzureVM 3e1d8f34-9fe7-6060-a2d0-7e3b7b16917f Succeeded |
Note to self: add the -WaitForBoot
switch to New-AzureVM
so it waits until the VM is ready (or provisioning fails). By default the cmdlet does not wait, and the VM will be in the Provisioning
status for a few minutes before it’s ready.
Step 5: Set up self-signed certificate for PowerShell remoting
I want to remotely connect to this machine – via PowerShell – and set it up as a DC.
By default PowerShell remoting is enabled on the VM over HTTPS and a self-signed certificate is installed in its certificate store. Since the machine we are connecting to the VM from does not know of this certificate, we must export this from the VM and add to the certificate store of this machine.
Here’s how you can view the self-signed certificate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
PS> Get-AzureCertificate -ServiceName LONSDC01 Url : https://management.core.windows.net/e2d94cf5-f086-4f18-9f8a-fd5fb663606a/services/hostedservices /LONSDC01/certificates/sha1-D5658EF700F9B4C0CCDB095E491EF4B84DDD4A09 Data : MIICFjCCAX+gAwIBAgIPDgAqABUADAALAAMAAgffMA0GCSqGSIb3DQEBBQUAMCAxHjAcBgNVBAMTFUxPTlNEQzAxLmNsb3Vk YXBwLm5ldDAeFw0xNTAyMDkxMjAwMDBaFw0yMzA0MjgxMjAwMDBaMCAxHjAcBgNVBAMTFUxPTlNEQzAxLmNsb3VkYXBwLm5l dDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7qqRcwrev1JdDTuCpq7HcamV6Mt3ksnKNgOOAjLOMbo0R+1xNm0xPlZN oNFN6qXCRkGPmNOtOttdVKUtEuDM91N/WWc5ZZKBGlSz8T9GuM6uDPrcN2q85own/rOll4cjiG2NLKhCZylHAIatmANoMyDE YVeLjnySnUXRAFXMPOsCAwEAAaNSMFAwHQYDVR0OBBYEFG8OfIOYzgvMFH3/r6/NkjDEClReMAsGA1UdDwQEAwIBtjATBgNV HSUEDDAKBggrBgEFBQcDATANBgNVHQoEBjAEAwIHgDANBgkqhkiG9w0BAQUFAAOBgQDOrfxeysQ9jIZnJF19Wuj/GAS2T43M vFYdIc4C22viiB1ckjYRNevGVXMbx3NAW7PSjJkjAhSqxyeTSU7wAA9Ev90JZ6bzEtLTUeqVnsFiPWIOpbOiIzdyr2ZpFg4j 0FUs5CN2PAmAvqAQnJh0AsYL0KsMTJE4eoAIScviLk6GsA== Thumbprint : D5658EF700F9B4C0CCDB095E491EF4B84DDD4A09 ThumbprintAlgorithm : sha1 ServiceName : LONSDC01 OperationDescription : Get-AzureCertificate OperationId : a9ef1ca1-32cd-77f2-bf06-ba4b673499da OperationStatus : Succeeded |
As an aside, it is possible to request a certificate with a specific fingerprint/ thumbprint with the above cmdlet. For that, you need to get the thumbprint of the certificate associated with the VM and specify that thumbprint via the -Thumbprint
switch. The following cmdlet pipe is an example of how to get the thumbprint associated with a VM:
1 2 3 |
PS> (Get-AzureVM -ServiceName LONSDC01 | select -ExpandProperty VM).DefaultWinRmCertificateThumbprint VERBOSE: 4:22:14 PM - Completed Operation: Get Deployment D5658EF700F9B4C0CCDB095E491EF4B84DDD4A09 |
The output of the Get-AzureCertificate
cmdlet contains the certificate in the Data
property. From my Linux days I know this is a Base64 encoded certificate. To import it into our certificate store let’s save this in a file (I chose the file extension cer
because that’s the sort of certificate this is):
1 |
PS> (Get-AzureCertificate -ServiceName LONSDC01).Data | Out-File LONSDC01.cer |
Then I import it into my Trusted Certificates store:
1 2 3 4 5 6 7 8 9 |
PS> Import-Certificate -FilePath .\LONSDC01.cer -CertStoreLocation Cert:\LocalMachine\root Directory: Microsoft.PowerShell.Security\Certificate::LocalMachine\root Thumbprint Subject ---------- ------- D5658EF700F9B4C0CCDB095E491EF4B84DDD4A09 CN=LONSDC01.cloudapp.net |
Finally, to test whether I can remotely connect to the VM via PowerShell, get the port number and try connecting to it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
PS> Get-AzureVM "LONSDC01" | Get-AzureEndpoint | ?{ $_.Name -eq "PowerShell" } LBSetName : LocalPort : 5986 Name : PowerShell Port : 61368 Protocol : tcp Vip : 111.221.91.26 ProbePath : ProbePort : 0 ProbeProtocol : ProbeIntervalInSeconds : ProbeTimeoutInSeconds : EnableDirectServerReturn : False Acl : {} InternalLoadBalancerName : IdleTimeoutInMinutes : LoadBalancerDistribution : PS> Invoke-Command -port 61368 -ComputerName lonsdc01.cloudapp.net -UseSSL -Credential (Get-Credential) -ScriptBlock { ipconfig } cmdlet Get-Credential at command pipeline position 1 Supply values for the following parameters: Credential Windows IP Configuration Ethernet adapter Ethernet 2: Connection-specific DNS Suffix . : LONSDC01.i8.internal.cloudapp.net Link-local IPv6 Address . . . . . : fe80::483f:1459:e1b6:b8fa%21 IPv4 Address. . . . . . . . . . . : 192.168.10.4 Subnet Mask . . . . . . . . . . . : 255.255.255.128 Default Gateway . . . . . . . . . : 192.168.10.1 Tunnel adapter isatap.LONSDC01.i8.internal.cloudapp.net: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : LONSDC01.i8.internal.cloudapp.net Tunnel adapter Teredo Tunneling Pseudo-Interface: Connection-specific DNS Suffix . : IPv6 Address. . . . . . . . . . . : 2001:0:9d38:6ab8:2c7b:3ea7:3f57:f5fb Link-local IPv6 Address . . . . . : fe80::2c7b:3ea7:3f57:f5fb%14 Default Gateway . . . . . . . . . : :: |
To summarize this step, here’s all the above cmdlets in one concise block (the last cmdlet is optional, it is for testing):
1 2 3 4 5 6 |
$AzureVM = "LONSDC01" (Get-AzureCertificate -ServiceName $AzureVM).Data | Out-File -FilePath .\${AzureVM}.cer Import-Certificate -FilePath .\${AzureVM}.cer -CertStoreLocation Cert:\LocalMachine\root $AzurePort = (Get-AzureVM $AzureVM | Get-AzureEndpoint | ?{ $_.Name -eq "PowerShell" }).Port $AzureCreds = Get-Credential Invoke-Command -port $port -ComputerName "${AzureVM}.cloudapp.net" -ScriptBlock { ipconfig } -UseSSL -Credential $AzureCreds |
Update: While writing this post I discovered a cmdlet Get-AzureWinRMUri which can be used to easily the WinRM end point URI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
PS> Get-AzureWinRMUri -ServiceName LONSDC01 AbsolutePath : / AbsoluteUri : https://lonsdc01.cloudapp.net:61368/ LocalPath : / Authority : lonsdc01.cloudapp.net:61368 HostNameType : Dns IsDefaultPort : False IsFile : False IsLoopback : False PathAndQuery : / Segments : {/} IsUnc : False Host : lonsdc01.cloudapp.net Port : 61368 Query : Fragment : Scheme : https OriginalString : https://lonsdc01.cloudapp.net:61368/ DnsSafeHost : lonsdc01.cloudapp.net IsAbsoluteUri : True UserEscaped : False UserInfo : |
Thus I can replace the above cmdlets like this:
1 2 3 4 |
$AzureVM = "LONSDC01" (Get-AzureCertificate -ServiceName $AzureVM).Data | Out-File -FilePath .\${AzureVM}.cer Import-Certificate -FilePath .\${AzureVM}.cer -CertStoreLocation Cert:\LocalMachine\root Invoke-Command -ConnectionUri $(Get-AzureWinRMUri -ServiceName $AzureVM) -Credential (Get-Credential) -ScriptBlock { ipconfig } |
Or start a remote session:
1 |
Enter-PSSession -ConnectionUri $(Get-AzureWinRMUri -ServiceName $AzureVM) -Credential (Get-Credential) |
Neat!
Step 6: Create a domain and promote this machine to DC
If I had started a remote session to the Azure VM, I can install the AD role onto it and create a domain.
1 2 3 4 |
Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools -Restart $ADPassword = Read-Host -Prompt "AD Password? (will be shown on screen)" $ADPasswordSS = ConvertTo-SecureString -String $ADPassword -AsPlainText -Force Install-ADDSForest -DomainName AzureLab.local -DomainNetbiosName AzureLab -SafeModeAdministratorPassword $ADPasswordSS -DomainMode Win2012R2 -ForestMode Win2012R2 -InstallDns -NoDnsOnNetwork -Force |
Alternatively I could put the above into a scriptblock and use the Invoke-Command
cmdlet.
1 2 3 4 5 6 7 8 |
$InstallAD = { Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools -Restart $ADPassword = Read-Host -Prompt "AD Password? (will be shown on screen)" $ADPasswordSS = ConvertTo-SecureString -String $ADPassword -AsPlainText -Force Install-ADDSForest -DomainName AzureLab.local -DomainNetbiosName AzureLab -SafeModeAdministratorPassword $ADPasswordSS -DomainMode Win2012R2 -ForestMode Win2012R2 -InstallDns -NoDnsOnNetwork -Force } Invoke-Command -ConnectionUri $(Get-AzureWinRMUri -ServiceName $AzureVM -Name $AzureVM) -Credential (Get-Credential) -ScriptBlock $InstallAD |
Note that once the VM is promoted to a DC it gets reboot automatically.
We now have three sites in Azure, and a VM in one of these sites that is also the DC for a newly created domain. There’s more stuff to be done, which I’ll return to in a later post.
Meanwhile, the other day I came across a blog post on securing Azure machines. Turns out it is common to probe all open ports of Azure VMs and try connecting to them via RDP and brute-forcing the password. The Azure VM created above does not use the default “administrator” username, but as a precaution I will remote the Remote Desktop endpoint now. When needed, I can add the endpoint later.
To remove the endpoint:
1 |
Get-AzureVM -ServiceName $AzureVM | Remove-AzureEndpoint -Name RemoteDesktop | Update-AzureVM |
To add the endpoint (port 11111 will be the Remote Desktop port):
1 |
Get-AzureVM -ServiceName $AzureVM | Add-AzureEndpoint -Name RemoteDesktop -Protocol TCP -LocalPort 3389 -PublicPort 11111 |
Check out that post for more ways of securing Azure VMs. Some day I’ll follow their suggestion of disabling Remote Desktop entirely and using VPN tunnels from my local machine to the Azure network.