In the last couple of weeks I have been working with a client that needed to deploy a two-tier web application that would act as a web store and they predicted that on the launch day about 1500-3000 users would hit the store. Now with that amount of client connections hitting one server would have been a complete failure so we needed to set up multiple web servers and configure Azure’s load balancer to balance the incoming traffic to multiple VMs. Now with the theory in place we just needed to find out how many frontend VMs were needed and after analyzing the facts we settled on the magic number of 10 front facing VMs with one beefy VM on the backend.

After having all the details now comes the hard part. How do I NOT deploy that many VMs manually? I chose not to go with the Azure Resource Manager for this project because I’m not yet convinced that ARM is ready for production deployments so ARM templates were off the plate. Now I didn’t have anything that came even remotely close to help me deploy the much needed VMs so I started PowerShell ISE and wrote a simple script that would create the VMs, create the needed endpoints and add a data disk, set the provisioned IP as static and so on. After an two hours or so I finished the script and ran it to see what happens and the result was OK but the script wasn’t modular enough and it served only one specific deployment and that didn’t help me at all in the long run. So after deploying and configuring all the VMs I started working on turning the script into a function that would help me cover more that one scenario.

After writing on paper what I needed from the script, I started working on it and here it is 🙂

Basically the script serves one purpose and that is to deploy a large amount of VMs in a Resource Group that will be set up as web servers. The script can deploy Windows or Linux VMs, all you need is to specify the VM Image of choice what type of VM you’re deploying. While I was developing the script I faced a problem in getting the VM Image name right so I removed that problem from the equation by adding a way to open a grid view that showed all the available VM images by label and pass them to the VMIMage variable. So if the $VMImage variable is not filled then the list comes up and you can choose what you want to deploy.

Vmtype

There are two problems that I couldn’t solve the way I wanted. Due to the way Azure works now by grouping up everything in Resource Groups (which is a very nice feature) and that sooner or later the Azure team will close the old portal (manage.windowsazure.com), if you want everything in one resource group and not deal with the hassle of creating virtual networks in PowerShell, you need to manually create the Resource Group, Virtual Network and Storage Account and the cloud service name be the same as the Resource Group and there’s no way around it.

Now with that out of the way. The best way to use the script is by using a method called splatting, which in short means that you create a hash table with all the parameters filled and pass them as a variable.

Here’s an example:

        $Deployment = @{
        SubscriptionID= '2c34969d-7d15-461a-91c5-d18819a3729d';
        Location = 'West Europe';
        ServiceName = 'AService';
        VMType = 'Linux';
        VMName ='A-Linux-VM-00';
        VirtualNetwork = 'VNETLinux'
        VNetSubnet = 'VNetSubnet';
        StorageAccount = 'linuxstorageaccount';
        VMSize = 'ExtraSmall';
        NumberOfVMInstances = 5;
        HostCaching = 'ReadWrite'
        VMImage ='OpenLogic 7.1'
        }
        New-AzureMassDeployment @Deployment -Verbose

As you can see from the example above that splatting makes the script be easier to read and manage compared to something like this:

New-AzureMassDeployment -SubscriptionID <SubID> -Location '<location>' -ServiceName '<Servicename>' -VMName '<VMNAME>' -VMSize '<VMSize>' -VMType '<Type>' -StorageAccount '<StorageAccountName>' -NumberOfVMInstances <number> -VirtualNetwork '<VNETNAME>' -VNetSubnet '<VNETSUBNETNAME>'

In any way, you can chose what ever method you want in order to use the script.

In closing, the script deploys and configures 10 VMs in about 58 minutes, if you do it manually it can take up to 6 hours which is quite a brain number, believe me, I tried it.

Hope this script solves a problem or two 🙂

The Script:

<#
        .Synopsis
        This script will deploy a number of ASM Azure VMs that will be deployed in a Resource Group and configured for load balacing on ports 80 and 443
        .DESCRIPTION
        New-AzureMassDeployment deploys multiple VMs configured for load balancing web content on ports 80 and 443.
        Once run the script will prompt will check if the provided storage account, virtual network and cloud service exists. If the storage account and virtual network do no exist, the script will halt.
        After it passes the first checks it will then prompt for a set of credentials that will be used to login to the VMs and after that it will open a grid view where the user will
        select the image SKU that will be used for the deployment. The grid view is filtered to show only the image SKUs that are available for the provided VMType "Windows or Linux"
        It will then proceed to fetch the image SKU that was previosly selected and start the provisioning of the VMs.
        In the creation process, the VMs are also updated to use their assigned DHCP internal IP as a static IP.

        The best way to use the script is by using PowerShell Splatting as show in the second example.
        .PARAMETER SubscriptionID
        Specify the subscription ID in which you will deploy the virtual machines
        .PARAMETER Location
        Specify the datacenter region in which you want the virtual machines deployed
        .PARAMETER ServiceName
        The name of the Cloud Service where the virtual machines will be deployed
        .PARAMETER VirtualNetwork
        The name of the Virtual Network where the virtual machines will reside
        .PARAMETER VNetSubnet
        The name of the Virtual Network subnet
        .PARAMETER VMType
        Specify which type of VM you require - Windows or Linux
        .PARAMETER VMName
        The name of the Virtual Machines
        .PARAMETER StorageAccount
        The name of the Storage Account where the Virtual Machines OS and Data Disk VHDs will reside
        .PARAMETER VMSize
        Virtual Machine Instance Size
        .PARAMETER NumberOfVMInstances
        Specify the number of virtual machines to be created and added to the load balancer
        .PARAMETER AvailabilityGroup
        Optional - The name of the Availability Group
        .PARAMETER HTTPLoadBalancedName
        Optional - Specify the label for the port 80 load balancer endpoint
        .PARAMETER HTTPSLoadBalancedName
        Optional - Specify the label for the port 443 load balancer endpoint
        .PARAMETER HostCaching
        Specify if you want caching to be done on the local disk drive - None, ReadOnly, ReadWrite
        .PARAMETER VMImaage
        Optional - Specify a VM Image label that will we used to create the virtual machines. If not specified a grid view will open prompting the user to chose an image.
        .NOTES 
        This script has two caveats, due to the way Azure now works with IaaS V2, every new ASM deployment is put in a resource group and the user is required to manually create the Resource Group, Virtual Network
        and Storage Account.
        .EXAMPLE
        New-AzureMassDeployment -SubscriptionID <SubID> -Location '<location>' -ServiceName '<Servicename>' -VMName '<VMNAME>' -VMSize '<VMSize>' -VMType '<Type>' -StorageAccount '<StorageAccountName>' -NumberOfVMInstances <number> -VirtualNetwork '<VNETNAME>' -VNetSubnet '<VNETSUBNETNAME>'
        .EXAMPLE
        $Deployment = @{
        SubscriptionID= 'SubID';
        Location = 'Location';
        ServiceName = 'ServiceName';
        VMType = 'Windows or Linux';
        VMName ='VMPrefix';
        VirtualNetwork = 'VMNetworkName'
        VNetSubnet = 'VMSubnet';
        StorageAccount = 'StorageAccount';
        VMSize = 'Small';
        NumberOfVMInstances = 1;
        HostCaching = 'ReadWrite'
        VMImage ='OpenLogic 7.1'
        }
        New-AzureMassDeployment @Deployment -Verbose


#>

#requires -Version 3 -Modules Azure
$VerbosePreference = 'Continue'
function  New-AzureMassDeployment
{
    [CmdletBinding(SupportsShouldProcess = $true, 
            PositionalBinding = $false,
    ConfirmImpact = 'Medium')]
   
    [OutputType([String])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $SubscriptionID,
        
        [Parameter(Mandatory = $true)]
        [ValidatePattern('[a-z]*')]
        [ValidateSet('Central US', 'South Central US', 'East US', 'West US', 'North Central US', 'East US 2', 'North Europe', 'West Europe', 'Southeast Asia', 'East Asia', 'Japan West', 'Japan East', 'Brazil South')]
        [String]
        $Location,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ServiceName,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateLength(1,15)]
        [String]
        $VMName,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
                    If ($_ -cmatch "^[^A-Z]*$") 
                    {
                        $true
                    }
                    else 
                    {
                        Throw "The Storage account parameter can only contain lowercase letters and numbers. Name has to be between 3 and 24 characters. Storage account provided: $_"
                    }
        })]
        [String]
        $StorageAccount,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $VMSize,
        
        [Parameter(Mandatory = $false)]
        [String]
        $VMType = 'Windows',
                                           
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Int]
        $NumberOfVMInstances = 2,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $VirtualNetwork,
                
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $VNetSubnet = 'default',
                
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $HTTPLoadBalancedName = 'HTTP-LBSet',
        
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $HTTPSLoadBalancedName = 'HTTPS-LBSet',
      
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $AvailabilityGroup = 'WEB-AVGroup',
        
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('None','ReadOnly', 'ReadWrite')]
        [String]
        $HostCaching = 'None',
        
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $VMImage

    )

    Begin
    {
        
        Write-Verbose -Message "$(Get-Date -Format T) -  Selecting the Azure Subscription"
        Select-AzureSubscription -SubscriptionId $SubscriptionID
        
        try
        {
            Set-AzureSubscription -SubscriptionId $SubscriptionID -CurrentStorageAccountName $StorageAccount -ErrorAction Stop
        }
        catch
        {
            "Error was $_"
            $line = $_.InvocationInfo.ScriptLineNumber
            "Error was in Line $line"
            break
        }
            
        Write-Verbose -Message "$(Get-Date -Format T) - Verifying if the virtual network: $VirtualNetwork exists"
        
        $null = Get-AzureVNetSite -VNetName "Group $ServiceName $VirtualNetwork" -ErrorAction Stop -ErrorVariable VnetMissing -OutVariable VNET
        
        if ($VirtualNetwork -eq $null)
        {
            throw $_
            Write-Error $VnetMissing
            break
        }
        else
        {
            $VNET = "Group $ServiceName $VirtualNetwork"
        }
        
        try
        {
            $null = Get-AzureService -ServiceName $ServiceName -ErrorAction Stop
        }
        catch
        {
            $ErrorMessage = $_.Exception.Message
                     
            if($PSCmdlet.ShouldProcess($ServiceName))
            {
                Write-Verbose -Message "$(Get-Date -Format T) - A cloud service was not found...Creating a cloud service named $ServiceName"
                New-AzureService -ServiceName $ServiceName -Location $Location 
            }
        }
                       
        $Credentials = Get-Credential -Message 'Type the name and password for the initial account.'

        if ($Credentials -eq $null)
        {
            Write-Error -Message 'No credentials have been supplied.'
            break
        }
        
        if ($VMImage -eq '')
        {
            Write-Verbose -Message "$(Get-Date -Format T) - Fetching a list of $VMType images"
            $GetLabelName = Get-AzureVMImage |
            Where-Object -Property OS -EQ -Value "$VMType" |
            Select-Object -Property Label, PublishedDate |
            Out-GridView -PassThru
            $Label = $GetLabelName.Label
            $Image = Get-AzureVMImage |
            Where-Object -FilterScript {
                $_.Label -eq $Label
            } |
            Sort-Object -Property PublishedDate -Descending |
            Select-Object -ExpandProperty ImageName -First 1
        }
        else
        {
            Write-Verbose -Message "$(Get-Date -Format T) - Fetching the VMImageName for $VMImage"
            $Image = Get-AzureVMImage |
            Where-Object -FilterScript {
                $_.Label -eq $VMImage
            } |
            Sort-Object -Property PublishedDate -Descending |
            Select-Object -ExpandProperty ImageName -First 1 -ErrorAction Stop
        }


        Write-Verbose -Message "$(Get-Date -Format T) - Setting HTTP and HTTPS ports and protocols"
        
        $Protocol = 'tcp'
        $ProbeProtocol = 'tcp'
        $HTTPPort = 80
        $HTTPPublicPort = 80
        $HTTPProbePort = 80
        $HTTPEndpointName = 'LB-HTTP'

        $HTTPSPort = 443
        $HTTPSPublicPort = 443
        $HTTPSProbePort = 443
        $HTTPSEndpointName = 'LB-HTTPS'
    
    }
    Process
    {
        $StartTime = Get-Date
        Write-Verbose -Message "$(Get-Date -Format T) - Preparing to create the VMs"
        for ($i = 10; $i -le $NumberOfVMInstances; $i++) 
        {
            $VMs = $VMName.ToString() + $i.ToString()
            $VMs = New-AzureVMConfig -Name $VMs -InstanceSize $VMSize -ImageName $Image -AvailabilitySetName $AvailabilityGroup
            
            if ($VMType -eq 'Windows')
            {
                $VMs | Add-AzureProvisioningConfig -Windows -AdminUsername $Credentials.UserName -Password $Credentials.Password
            }
            else
            
            {
                $VMs | Add-AzureProvisioningConfig -Linux -LinuxUser $Credentials.GetNetworkCredential().Username -Password $Credentials.GetNetworkCredential().Password
            }
            $DiskSize = 1023
            $DiskName = $VMName.ToString() + $i.ToString() + '-datadisk'
    
            $VMs | Set-AzureSubnet -SubnetNames $VNetSubnet
            $VMs | Add-AzureDataDisk -CreateNew -DiskLabel $DiskName -DiskSizeInGB $DiskSize -LUN 0 -HostCaching $HostCaching                  
                        
            $j = $i - 1
                       
            $PercentComplete = ($j / $NumberOfVMInstances) * 100
            
            $ProgressParameters = @{
                Activity         = 'Creating VMs'
                Status           = "Creating VM number $i"
                CurrentOperation = "$PercentComplete% complete"
                PercentComplete  = $PercentComplete
            }
                        
            Write-Progress @ProgressParameters
            
            if ($PSCmdlet.ShouldProcess($ServiceName))
            {
                New-AzureVM -ServiceName $ServiceName -VMs $VMs -VNetName "$VNET" -WaitForBoot -ErrorAction Stop
            }
            Write-Verbose -Message "$(Get-Date -Format T) - Waiting for the VM to fully boot"
            
            Start-Sleep 15            
            
            AzureVMName = $VMName.ToString() + $i.ToString()
            $AzureVM = Get-AzureVM -ServiceName $ServiceName -Name $VMName$i
            
            Write-Verbose -Message "$(Get-Date -Format T) - Setting a static IP address on $VMName$i"
            if ($PSCmdlet.ShouldProcess($AzureVM))
            {
                Set-AzureStaticVNetIP -VM $AzureVM -IPAddress ($AzureVM.IpAddress).ToString()
            }
        
            Add-AzureEndpoint -Name $HTTPEndpointName -Protocol $Protocol -LocalPort $HTTPPort -PublicPort $HTTPPublicPort -LBSetName $HTTPLoadBalancedName -ProbeProtocol $ProbeProtocol -ProbePort $HTTPProbePort -VM $AzureVM
            Add-AzureEndpoint -Name $HTTPSEndpointName -Protocol $Protocol -LocalPort $HTTPSPort -PublicPort $HTTPSPublicPort -LBSetName $HTTPSLoadBalancedName -ProbeProtocol $ProbeProtocol -ProbePort $HTTPSProbePort -VM $AzureVM
            Update-AzureVM
        }
        
        $EndTime = Get-Date
        
        $EndCompare = New-TimeSpan -Start $StartTime -End $EndTime
        
        Write-Output -InputObject ('The deployment was completed in ' +$EndCompare.Minutes+ ' minutes')
        }
}

Pin It on Pinterest