Create a Load Balanced Azure IAAS Virtual Machine (VM) with an RDP connection

Overview

A very common scenario in Azure Infrastructure As A Service (IAAS) is to have two or more VM’s behind an Azure external Internet-facing load balancer.  The load balancer provides high availability and network performance by distributing traffic according to a predetermined algorithm.

In this exercise we will create a load-balanced VM in Azure – using Powershell.  And we will enable RDP access to the VM by creating a NAT rule.

Details

In our scenario, we are creating one VM and the load balancer is forwarding Internet traffic to the VM by using an RDP NAT rule that maps an Azure public IP address to a private IP address.  The private IP address is on the Network Interface (NIC) card attached to the VM.

Consequently, no Load Balancing rules are created or used – only a NAT rule – to allow RDP into the VM.

Please notice that the script below creates a second data disk on the VM (because the VM is to be a Domain Controller).  You can easily remove the code for the second disk if it is not needed.

break

Login-AzureRmAccount
Get-AzureRmSubscription
Select-AzureRmSubscription -SubscriptionId <your subscription ID>

#################################################
# Create Resource Group - or use an existing one
#################################################
$loc = 'westeurope'
$rgName = 'RG-ASRConfigServer'
New-AzureRmResourceGroup -Name $rgname -Location $loc -Force

# Create a storage account
$stoName = 'storasrconsrvr4'
$stoType = 'Standard_LRS'
$naResult = Get-AzureRmStorageAccountNameAvailability $stoName

if ($naResult.NameAvailable) {
New-AzureRmStorageAccount -ResourceGroupName $rgName -Name $stoName `
                          -Location $loc -SkuName $stotype -Kind Storage
}
else { # The LoadBalancer is not used to create Load Balancing rules
# But to create NAT rules (to allow RDP from the Internet)
 Write-Host ''
 Write-Host -ForegroundColor Yellow "This storage account name: $stoName is not available"
 break
}
$storageAccount = Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name $stoName

###############################
# Set up networking for the VM
###############################
# Create VNet and Subnet(s) - if needed
$beSubnetName = 'LB-SUBNET-BE'
$backendSubnet = New-AzureRmVirtualNetworkSubnetConfig -Name $beSubnetName `
                                                        -AddressPrefix '10.0.0.0/24'
$vnetName = 'vnetConfserv'
$vnet = New-AzureRmVirtualNetwork -Name $vnetName -ResourceGroupName $rgname `
                                  -Location $loc -AddressPrefix '10.0.0.0/16' `
                                  -Subnet $backendSubnet 

#Create Public IP address 
#########################
$ipName = 'confservPublicIP'
$locName = 'West Europe'
$domainName = 'asrconfig'
$pip = New-AzureRmPublicIpAddress -Name $ipName -ResourceGroupName $rgName `
                                  -Location $locName -AllocationMethod Dynamic `
                                  -DomainNameLabel $domainName

#Create a front-end IP address pool tied to the Public IP address
#################################################################
$frontendIP = New-AzureRmLoadBalancerFrontendIpConfig -Name 'LB-FrontendIP' `
                                                      -PublicIpAddress $pip

#Create a back-end IP address pool that will be tied to the NIC
###############################################################
$backendAP = New-AzureRmLoadBalancerBackendAddressPoolConfig -Name 'LB-BackendAP'

#Create an Inbound NAT rule for Remote Desktop
##############################################
$natRuleRDP = New-AzureRmLoadBalancerInboundNatRuleConfig -Name 'natRuleRDP' `
                            -FrontendIpConfiguration $frontendIP -Protocol TCP `
                            -FrontendPort 3442 -BackendPort 3389 

#Create the external Load Balancer
##################################
$lbName = 'vmLoadBalancer'
$loadBal = New-AzureRmLoadBalancer -ResourceGroupName $rgName -Name $lbName `
                               -Location 'West Europe' -FrontendIpConfiguration $frontendIP `
                               -InboundNatRule $natRuleRDP -BackendAddressPool $backendAP

# Attach backend address pool to load balancer                                 
Add-AzureRmLoadBalancerBackendAddressPoolConfig -Name $backendAP -LoadBalancer $loadBal | `
                                                Set-AzureRmLoadBalancer
                                                     
#Create the Network Interface
#############################

$vnet1 = Get-AzureRmVirtualNetwork -Name $vnetName -ResourceGroupName $rgName
$backendSubnet1 = Get-AzureRmVirtualNetworkSubnetConfig -Name $beSubnetName -VirtualNetwork $vnet1
$nicName = 'vmNIC'
$vmNIC = New-AzureRmNetworkInterface -ResourceGroupName $rgName -Name $nicName `
                             -Location $locName -PrivateIpAddress '10.0.0.4' `
                             -Subnet $backendSubnet1 `
                             -LoadBalancerBackendAddressPool $loadBal.BackendAddressPools[0] `
                             -LoadBalancerInboundNatRule $loadBal.InboundNatRules[0]

##############################################
#Create the VM
##############################################
$vmName = 'vm-asrcsrvr'
$AvID = (New-AzureRmAvailabilitySet -ResourceGroupName $rgName `
                                    -Name 'adAvailabiltySet' -Location 'West Europe').id

#create a VM configuration
##########################
$vmObj = New-AzureRmVMConfig -VMName $vmName -VMSize 'Standard_A1' -AvailabilitySetId $AvID

$compName = 'vm-asrcsrvr'
$cred = Get-Credential -Message 'Enter name / password of VM administrator account.'
$vmObj = Set-AzureRmVMOperatingSystem -VM $vmObj -Windows -ComputerName $compName `
                                     -Credential $cred -ProvisionVMAgent `
                                     -EnableAutoUpdate

$vmObj = Set-AzureRmVMSourceImage -VM $vmObj -PublisherName MicrosoftWindowsServer `
                                 -Offer WindowsServer -Skus 2012-R2-Datacenter `
                                 -Version 'latest'

# Add the network interface (NIC) to the VM
###########################################
$vmObj = Add-AzureRmVMNetworkInterface -VM $vmObj -Id $vmNIC.Id

# ADD OS Disk and Data Disk
###########################
$blobPath = 'vhds/spfarm-ad-osdisk.vhd'
$osDiskUri = $storageAccount.PrimaryEndpoints.Blob.ToString() + $blobPath

$diskName = 'adVmOSDisk'
$vmObj = Set-AzureRmVMOSDisk -VM $vmObj -Name $diskName -VhdUri $osDiskUri `
                            -CreateOption fromImage -DiskSizeInGB 1000

# Add data disks by using the URLs of the copied data VHDs at the appropriate 
#  Logical Unit Number (Lun).
$dataDiskBlobPath = 'vhds/ADDataDisk-1.vhd'
$dataDiskUri = $storageAccount.PrimaryEndpoints.Blob.ToString() + $dataDiskBlobPath

# AD DC in Azure need the data disk storing sysvol to have NO caching
$vmObj = Add-AzureRmVMDataDisk -VM $vmObj -Name 'ADDataDisk-1' -Caching None `
                            -VhdUri $dataDiskUri -Lun 0 -DiskSizeInGB 1000 `
                            -CreateOption empty

# Create the actual VM
New-AzureRmVM -ResourceGroupName $rgName -Location $locName -VM $vmObj

######################
####Post VM Creation:
######################

# Add the network interface (NIC) to the load balancer
######################################################

$lb = Get-AzureRmLoadBalancer -Name $lbName -ResourceGroupName $rgName
$backend = Get-AzureRmLoadBalancerBackendAddressPoolConfig -Name $backendAP `
                                                           -LoadBalancer $lb
$nic = Get-AzureRmNetworkInterface -Name $nicName -ResourceGroupName $rgName
$nic.IpConfigurations[0].LoadBalancerBackendAddressPools = $backend
Set-AzureRmNetworkInterface -NetworkInterface $nic



Lessons Learned

1. If $backendSubnet is used instead of $backendSubnet1 and Error is produced: “Parameter set cannot be resolved using the specified named parameters” OR “Cannot parse the request”.

2. The Backend Address pool will contain the objects (IPs) targeted by the Load Balancer. If you want to redirect via NAT, The VM’s NIC should be part of the backend pool.  Therefore, you need to associate the Backend AddressPool with the IP address of the VM (held in the NIC).  This needs to be done after the VM is created.

Building an ADDS DC in Azure IAAS using ARM and DSC

Sometimes we may need to create an Active Directory Domain Services Domain Controller (DC) in an Azure IAAS Virtual Machine (VM).  For example, I recently set up an IAAS SharePoint farm in the cloud.  A DC on an Azure IAAS VM was a natural fit.

This post will discuss the steps required to deploy and provision such a DC in Azure – using Azure Resource Manager (ARM) templates combined with Powershell Desired State Configuration (DSC).

Brief Introduction to ARM templates and to Powershell Desired State Configuration (DSC)

ARM and DSC are tools that allow the description of infrastructure as code.  Infrastructure described in code allows you to deploy infrastructure consistently.  And it makes the build and deployment process repeatable.

ARM templates

ARM templates are JSON files that describe the resources (infrastructure components) that you need to deploy to Azure.  For example, a VM, a Web App, a VNET or a NIC etc.

DSC

On the other hand, Powershell DSC allows you to describe the end state of how you like your – on premises or Cloud – server to look like.  Once you have described your configuration with all the needed roles and features, DSC goes ahead and “makes it so”.  In other words, it provisions the server per your specifications.

ARM Templates & DSC

Both ARM templates and Powershell DSC provide the necessary tools for consistently deploying applications.  And consistently deployed applications are critical factors needed for DevOps.

Steps to create a DC in Azure

Overview of the Process
  1. Create a new Azure Resource Group project is VS.
  2. Add the Windows Virtual Machine template to the project.
  3. Subsequently, add the DSC Extension resource.
  4. A new DSC configuration data file (.psd1 aka Powershell Script data file) is added to the project.  Alternatively, the PS data file may be hosted online.
  5. Customize (edit) your JSON template files and your DSC files.
  6. Deploy the solution to Azure.
  7. Check on your Virtual Machine in Azure.  Remote Desktop to the VM and verify that ADDS, DNS and ADDS Recycle Bin roles and features are enabled.
Detailed Steps:

1. I created a New Project in Visual Studio and chose Azure Resource Group for the type:

2. I chose Windows Virtual Machine as the Template:

choose Azure Virtual Machine
choose Azure Virtual Machine

3. I opened the windowsvirtualmachine.json file.  In the JSON outline (left-hand side), I right-clicked on Resources and added a new resource – PowerShell DSC Extension:

Open VM json file

4. I added the Powershell Data file (.psd1).  However, I ended up not using it because the deployment script did not find it (see details below).  Instead, I uploaded the data file on Github and used a link (URL) to it in the JSON template.

add Powershell data file

5. I modified the WindowsVirtualMachine.json file adding the parameters and variables that will be used in the JSON document:

  "parameters": {
    "adminUsername": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Username for the Virtual Machine."
      }
    },
    "adminPassword": {
      "type": "securestring",
      "metadata": {
        "description": "Password for the Virtual Machine."
      }
    },
    "dnsNameForPublicIP": {
      "type": "string",
      "minLength": 1,
      "metadata": {
        "description": "Globally unique DNS Name for the Public IP used to access the Virtual Machine."
      }
    },
    "windowsOSVersion": {
      "type": "string",
      "defaultValue": "2012-R2-Datacenter",
      "allowedValues": [
        "2008-R2-SP1",
        "2012-Datacenter",
        "2012-R2-Datacenter"
      ],
      "metadata": {
        "description": "The Windows version for the VM. This will pick a fully patched image of this given Windows version. Allowed values: 2008-R2-SP1, 2012-Datacenter, 2012-R2-Datacenter."
      }
    },
    "_artifactsLocation": {
      "type": "string",
      "metadata": {
        "description": "Auto-generated container in staging storage account to receive post-build staging folder upload"
      }
    },
    "_artifactsLocationSasToken": {
      "type": "securestring",
      "metadata": {
        "description": "Auto-generated token to access _artifactsLocation"
      }
    },
    "DSC-VMDCUpdateTagVersion": {
      "type": "string",
      "defaultValue": "1.0",
      "metadata": {
        "description": "This value must be changed from a previous deployment to ensure the extension will run"
      }
    },
    "sizeOfDataDiskInGB": {
      "type": "int",
      "defaultValue": 100,
      "metadata": {
        "description": "Size of each data disk in GB"
      }
    },
    "FQDomainName": {
      "type": "string",
      "metadata": {
        "description": "Domain name to give to new ADDS Domain"
      }
    },
    "AdminUser1CredsParam": {
      "type": "securestring",
      "metadata": {
        "description": "Admin User password"
      }
    }
  },
  "variables": {
    "imagePublisher": "MicrosoftWindowsServer",
    "imageOffer": "WindowsServer",
    "OSDiskName": "osdiskforwindowssimple",
    "DataDiskName": "datadisk1nocache",
    "nicName": "myVMNic",
    "addressPrefix": "10.0.0.0/16",
    "subnetName": "Subnet",
    "subnetPrefix": "10.0.0.0/24",
    "vhdStorageType": "Standard_LRS",
    "publicIPAddressName": "myPublicIP",
    "publicIPAddressType": "Dynamic",
    "vhdStorageContainerName": "vhds",
    "vmName": "ARMDCVM",
    "vmSize": "Standard_A2",
    "virtualNetworkName": "MyVNET",
    "vnetId": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]",
    "subnetRef": "[concat(variables('vnetId'), '/subnets/', variables('subnetName'))]",
    "vhdStorageName": "[concat('vhdstorage', uniqueString(resourceGroup().id))]",
    "diagnosticsStorageAccountName": "[variables('vhdStorageName')]",
    "diagnosticsStorageAccountResourceGroup": "[resourcegroup().name]",
    "accountid": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', variables('diagnosticsStorageAccountResourceGroup'), '/providers/', 'Microsoft.Storage/storageAccounts/', variables('diagnosticsStorageAccountName'))]",
    "wadmetricsresourceid": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', variables('diagnosticsStorageAccountResourceGroup'), '/providers/', 'Microsoft.Compute/virtualMachines/', variables('vmName'))]",
    "DSC-VMDCArchiveFolder": "DSC",
    "DSC-VMDCArchiveFileName": "armdscvmdc.zip",
    "DSC-ConfigFile": "armdscvmdc.ps1",
    "DSC-ConfigDataFile": "armdscvmdc.psd1"
  },

6. I edited the Virtual Machine resource section to add a Data Disk.  A second disk (with caching off) is required for Domain Controllers in Azure.  ADDS database and SYSVOL must NOT be stored on an Azure OS disk type.

          "dataDisks": [
            {
              "name": "datadisk1",
              "diskSizeGB": "[parameters('sizeOfDataDiskInGB')]",
              "lun": 0,
              "vhd": {
                "uri": "[concat('https://', variables('vhdStorageName'), '.blob.core.windows.net/', variables('vhdStorageContainerName'), '/', variables('DataDiskName'), '.vhd')]"
              },
              "createOption": "Empty",
              "caching": "None"
            }
          ]

7. Subsequently, I edited DSC section of the JSON template to include: configurationArguments (parameters passed to the DSC script) and ConfigurationData (URL to .psd1 file on Github)

        {
          "name": "Microsoft.Powershell.DSC",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2015-06-15",
          "dependsOn": [
            "[resourceId('Microsoft.Compute/virtualMachines', variables('vmName'))]"
          ],
          "tags": {
            "displayName": "DSC-VMDC"
          },
          "properties": {
            "publisher": "Microsoft.Powershell",
            "type": "DSC",
            "typeHandlerVersion": "2.9",
            "autoUpgradeMinorVersion": true,
            "forceUpdateTag": "[parameters('DSC-VMDCUpdateTagVersion')]",
            "settings": {
              "configuration": {
                "url": "[concat(parameters('_artifactsLocation'), '/', variables('DSC-VMDCArchiveFolder'), '/', variables('DSC-VMDCArchiveFileName'))]",
                "script": "[variables('DSC-ConfigFile')]",
                "function": "Main"
              },
              "configurationArguments": {
                "nodeName": "[variables('vmName')]",
                "FQDomainName": "[parameters('FQDomainName')]"
              },
              "configurationData": {
                "url": "https://raw.githubusercontent.com/GhassanHariz/AzureRMDSC/master/DCVM-DSC.psd1"
              }
            },
            "protectedSettings": {
              "configurationArguments": {
                "DomainAdmin1Creds": {
                  "userName": "[parameters('adminUsername')]",
                  "password": "[parameters('adminPassword')]"
                },
                "AdminUser1Creds": {
                  "userName": "AdminUser1",
                  "password": "[parameters('AdminUser1CredsParam')]"
                }
              },
              "configurationUrlSasToken": "[parameters('_artifactsLocationSasToken')]"
            }
          }
        }

8. The parameters list was updated in WindowsVirtualMachines.parameters.json file:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "adminUsername": {
      "value": "srvradmin"
    },
    "dnsNameForPublicIP": {
      "value": "dscvmdc"
    },
    "windowsOSVersion": {
      "value": "2012-R2-Datacenter"
    },
    "sizeOfDataDiskInGB": {
      "value": 100
    },
    "DSC-VMDCUpdateTagVersion": {
      "value": "2.0"
    },
    "FQDomainName": {
      "value": "spdomain.local"
    }

  }
}

9. When the DSC Extension is added to your project, the DSC script provided contains an example configuration.  Since we are creating our own configuration, that example was removed and the following was added.  The following DSC configuration provisions the DC and adds an administrative user account.

Configuration Main
{
	Param(
		[string] $NodeName,

		[Parameter(Mandatory)]
		[String]$FQDomainName,

		[Parameter(Mandatory)]
		[PSCredential]$DomainAdmin1Creds,

		[Parameter(Mandatory)]
		[PSCredential]$AdminUser1Creds,

		[Int]$RetryCount=5,
		[Int]$RetryIntervalSec=30
		)

Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xActiveDirectory, `
                                    xComputerManagement, `
                                    xNetworking,
									xStorage
 
Node $AllNodes.Where{$_.Role -eq "DC"}.Nodename
       {
		LocalConfigurationManager 
        {
            ActionAfterReboot = 'ContinueConfiguration'            
            ConfigurationMode = 'ApplyOnly'            
            RebootNodeIfNeeded = $true  
			AllowModuleOverwrite = $true
        }

        xWaitforDisk Disk2
        {
             DiskNumber = 2
             RetryIntervalSec =$RetryIntervalSec
             RetryCount = $RetryCount
        }

        xDisk ADDataDisk
        {
            DiskNumber = 2
            DriveLetter = 'F'
        }

		# Add DNS
		WindowsFeature DNS
        {
            Ensure = "Present"
            Name = "DNS"
        }

        # Install the Windows Feature for AD DS
        WindowsFeature ADDSInstall {
            Ensure = 'Present'
            Name = 'AD-Domain-Services'
        }

        # Make sure the Active Directory GUI Management tools are installed
        WindowsFeature ADDSTools            
        {             
            Ensure = 'Present'             
            Name = 'RSAT-ADDS'             
        }           

        # Create the ADDS DC
        xADDomain FirstDC {
            DomainName = $FQDomainName
            DomainAdministratorCredential = $DomainAdmin1Creds
            SafemodeAdministratorPassword = $DomainAdmin1Creds
			DatabasePath = 'F:\NTDS'
            LogPath = 'F:\NTDS'
            SysvolPath = 'F:\SYSVOL'
            DependsOn = '[WindowsFeature]ADDSInstall'
        }   
        
        xWaitForADDomain DscForestWait
        {
            DomainName = $FQDomainName
            RetryCount = $RetryCount
            RetryIntervalSec = $RetryIntervalSec
            DependsOn = '[xADDomain]FirstDC'
        } 

        #
        xADRecycleBin RecycleBin
        {
           EnterpriseAdministratorCredential = $DomainAdmin1Creds
           ForestFQDN = $FQDomainName
           DependsOn = '[xADDomain]FirstDC'
        }
        
        # Create an admin user so that the default Administrator account is not used
        xADUser FirstUser
        {
            DomainAdministratorCredential = $DomainAdmin1Creds
            DomainName = $FQDomainName
            UserName = $AdminUser1Creds.UserName
            Password = $AdminUser1Creds
            Ensure = 'Present'
            DependsOn = '[xADDomain]FirstDC'
        }
        
        xADGroup AddToDomainAdmins
        {
            GroupName = 'Domain Admins'
            MembersToInclude = $AdminUser1Creds.UserName
            Ensure = 'Present'
            DependsOn = '[xADUser]FirstUser'
        }     
    }

}

10. The DSC configuration data file (.psd1) on Github contains:

# Configure all of the settings we want to apply for this configuration
@{
    AllNodes = @(
        @{
            NodeName = '*'
            PSDscAllowPlainTextPassword = $true
            PSDscAllowDomainUser = $true
        },
        @{ 
            Nodename = "localhost"
            Role = "DC"
        }
    )
}

11. Once all the JSON and DSC files are customized, the project can be deployed to Azure.  Right-click on the project and select Deploy-New.  Choose your Azure account and subscription.  Verify the parameters entered and click OK to deploy to the Azure Resource Group.

12. After about 30 minutes, the deployment will be completed.  Log in to the Azure portal and verify your DC is up and running.  You can Connect to the DC via Remote Desktop to view your newly created DC

Domain Controller Azure

Lessons learned

Why use Visual Studio?

Microsoft Visual Studio 2015 Community Edition is a wonderful development IDE and it is quite good for authoring ARM templates and the DSC scripts.  It goes ahead and deploys the whole solution to Azure and allows you to monitor the progress and catch any errors.

The advantage of VS for ARM and DSC

You can author the ARM templates and DSC scripts in any text editor or IDE and then deploy them using Powershell.  However, in my experience, deploying the templates and DSC code from VS provided more feedback about the progress of the deployment.  I do not have to add any parameters for verbose output and I do not have to check any log files.

Furthermore, VS is suitable for PowerShell DSC because you can add a VS Powershell extension (from the Visual studio Marketplace), that allows you to author and edit PS code.

Issues and Solutions

DSC Configuration Data File not being found during deployment

As mentioned above the .psd1 was originally added to the VS project.  Once it is added to VS, you have to right-click on the .psd1 file and select Properties.  For All Configurations select:

  • Build Action : Always
  • Copy to Ouput Directory: Copy Always

When I tried to change the Build Action, I received an error: An error occurred while saving the edited properties listed below: Build Action.  One or more values are invalid.  Mismatched PageRule with the wrong ItemType.

The workaround is to apply the second setting first (Copy Always).  Then apply the first setting (Build Action).

The above workaround was good and allowed the .psd1 file to be copied to the correct location in the staging area along with the .ps1 file and the Zip archive.  However, the deployment process still complained that the .psd1 file was not found “after 19 attempts”.

Online search on this error resulted in posts suggesting that a duplicate path in the PSModulePath environment variable may be the culprit.  However, none of the workarounds and suggestions allowed the .psd1 file to be found.

Workaround: place the .psd1 file online.  I placed the file on Github in order to get it deployed.