I was recently working on a proof of concept in Azure for a client that needed a couple of VMs to test if Azure is a viable candidate for their on-premise workloads. The client only needed those VMs at certain hours on weekdays and that meant that I needed to implement a method to remove any unnecessary run-time costs and that’s where Azure Automation and PowerShell comes in 🙂

For this particular proof of concept I chose to deploy ARM VMs and that was a good decision because one of the key differences between Azure Resource Manager and Azure Service Manager is parallelization and that’s a major difference. For example purposes the Classic model doesn’t let you start, stop, deploy multiple VMs or any other Azure goodies at the same time and if you try to do that you get a generic error like the one quoted below.

The virtual machine ‘VMNAME’ operation failed: ‘Windows Azure is currently performing an operation with x-ms-requestid on this deployment that requires exclusive access.

Now that you know that the ARM model can do everything in parallel, you can now start writing PowerShell Workflows that allow you to use the -parallel parameter in foreach loops which basically allows you to run multiple tasks at once. So if you want to start 100 VMs or create 100 Storage Accounts in say 5 minutes, then that’s the way to go.

The scripts I wrote are to be considered as samples because they target Resource Groups which means that all the Virtual Machines in a Resource Group will start or stop, so please keep that in mind. The scripts can be adapted to target specific VMs without doing any hard coding in the scripts and that can be achieved by tagging the VMs which is another feature that ARM provides.

ARM Tags

Changing the scripts to look for tags is very simple. Basically you add another mandatory parameter and add a filter in the Get-AzureRMVM cmdlet as shown below:

    param(
     
        [Parameter(Mandatory = $true)] 
        [string]$ResourceGroup,
        [Parameter(Mandatory = $true)] 
        [string]$SubscriptionCredential,
        [Parameter(Mandatory = $true)] 
        [string]$VMTag
    
    )

#$VMs = Get-AzureRmVM -ResourceGroupName "$ResourceGroup" 

$VMs = Get-AzureRmVM -ResourceGroupName "$ResourceGroup" | Where-Object {$_.Tags.Keys -EQ $VMTag}

All you need to do in order to make them work is to have an Azure Automation Account, an AAD user that’s co-administrator on the subscription to be configured as a credential asset in the automation account, the runbooks and a schedule or more.

Everything that I just mentioned can be done via the GUI or you can use PowerShell. I’ll add a extra script in this post that creates everything that you need to make them work but with one exception and that’s the creation of the AAD user and adding him to the subscription as co-admin. I chose to not include that part in the script because it requires an additional Azure Module to be installed and doing that for one single step is not worth it.

Here’s the script:

<#
        .Synopsis
        Preparation script for Azure Automation
        .DESCRIPTION
        This script will automate the steps required to create the necesary variables, accounts and credentials required for Azure Automation to work as intended.
        The script is easy to use as shown in the examples. The -StorageContainer parameter is set to false by default and if required it can be set to true by adding it to the final line and setting $true to it.

        .PARAMETER AutomationAccount
        Mandatory variable used to create a free Azure Automation account. The variable takes string input and that string input will be the name that will appear in the Azure Portal.

        .PARAMETER Location
        Mandatory parameter used to specify in which Azure datacenter should the automation resources reside
        
        .PARAMETER AzureUser
        Mandatory parameter that is used to specify the Azure co-admin user account that will be used to perform the automation tasks.

        .PARAMETER SubscriptionID
        Mandatory parameter that requires the subscription ID in order to automate tasks. Azure Automation requires either the subscription name or the ID and in some cases both, depending on the complexity of the 
        automation runbook.

        .PARAMETER ResourceGroup
        Mandatory parameter that requires a resource group name that will be created or that already exists.

        .PARAMETER SubscriptionName
        Same as the SubscriptionID, this mandatory parameter requires the name of the Azure subscription eg: 'Azure in Open', 'Pay as you Go', 'Visual Studio Ultimate with MSDN' etc.
           
        .EXAMPLE
        Set-AzureRMVariables -ResourceGroup '<NAME>' -AutomationAccount '<NAME>' -Location 'West Europe' -AzureUser 'AzureAutomationAccount' -SubscriptionID <SubscriptionGUID> -SubscriptionName '<Subscription Name>'

        
#>
function  Set-AzureRMVariables
{
    [CmdletBinding(SupportsShouldProcess = $true, 
            PositionalBinding = $false,
    ConfirmImpact = 'Medium')]
   
    [OutputType([String])]
    Param
    (
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $AutomationAccount,
        
        [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]
        $AzureUser,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $SubscriptionID,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ResourceGroup,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String]
        $SubscriptionName
        
    )

    Begin
    {
        Write-Verbose -Message "Selecting the azure subscription - $SubscriptionName"
        Select-AzureRmSubscription -SubscriptionId $SubscriptionID -ErrorAction Stop
        
        Write-Verbose -Message "Checking if $ResourceGroup exists."
        
        $RGCheck = Get-AzureRmResourceGroup -Name $ResourceGroup -ErrorAction SilentlyContinue
        if (!$RGCheck)
        {
            Write-Warning -Message "Specified Resource Group does not exist. Creating the Resource Group: $ResourceGroup"
            New-AzureRmResourceGroup -Name  $ResourceGroup -Location $Location
        }


    }
    Process
    {

        Write-Verbose -Message 'Creating the Azure resources that will be used for automation'
        New-AzureRmAutomationAccount -ResourceGroupName $ResourceGroup -Name $AutomationAccount -Location $Location  -ErrorAction Stop
        New-AzureRmAutomationCredential -ResourceGroupName $ResourceGroup -Name $AzureUser -AutomationAccountName $AutomationAccount -Value (Get-Credential) 
        New-AzureRmAutomationVariable -ResourceGroupName $ResourceGroup -Name 'SubscriptionID' -AutomationAccountName $AutomationAccount -Value $SubscriptionID  -Encrypted $false
        New-AzureRmAutomationVariable -ResourceGroupName $ResourceGroup -Name 'SubscriptionName' -AutomationAccountName $AutomationAccount -Value $SubscriptionName -Encrypted $false

    }
}

In order to run the scripts on your local machine, you need to have the latest Azure ARM PowerShell module which at the time of writing is at version 1.0.4. You can install the Azure ARM PowerShell module using PowerShell v5 if you’re already running Windows 10 by running Install-Module AzureRM and then Install-AzureRM cmdlets or you can get PowerShell V5 on older operating systems by installing WMF 5.
Windows Management Framework 5 can be installed on Windows Server 2012 R2, Windows Server 2012, Windows 2008 R2 SP1, Windows 8.1 and Windows 7 SP1 but at the time of writing this post WMF5 was pulled from the download center because of a bug that resets the PowerShell Module Environment to the default settings. You can read more about it here -> Blog post on WMF5 issue

Later Edit: WMF5 is back! Here’s the download link:

Now without further ado, here are the scripts 🙂

Merry Christmas!

#requires -Version 3 -Modules AzureRM.Compute, AzureRM.Profile
workflow Start-Azure-VM
{
    param(
     
        [Parameter(Mandatory = $true)] 
        [string]$ResourceGroup,
        [Parameter(Mandatory = $true)] 
        [string]$SubscriptionCredential,
        [Parameter(Mandatory = $false)] 
        [string]$AzureSubscriptionID
    )
    # Stop the script if run in the weekend
    $CurrentDay = (Get-Date).DayOfWeek
    if ($CurrentDay -eq 'Saturday' -or $CurrentDay -eq 'Sunday')
    {
        Write-Output -InputObject 'Not a business day, canceling the procedure'
        exit
    }
    $Credential = Get-AutomationPSCredential -Name $SubscriptionCredential
	$SubscriptionID = Get-AutomationVariable -Name $AzureSubscriptionID
    Add-AzureRmAccount -Credential $Credential
  
    Select-AzureRmSubscription -SubscriptionId $SubscriptionID

    $VMs = Get-AzureRmVM -ResourceGroupName "$ResourceGroup"
 
    # Start VMs in parallel 
    if(!$VMs) 
    {
        Write-Output -InputObject 'No VMs were found in the specified Resource Group.'
    }
    else 
    {
        ForEach -parallel ($VM in $VMs) 
        {
            $StartVM = Start-AzureRmVM -ResourceGroupName "$ResourceGroup" -Name $VM.Name -ErrorAction SilentlyContinue
            
            $Attempt = 1
            if(($StartVM.StatusCode) -ne 'OK')
            {
                do
                {
                    Write-Output -InputObject "Failed to start $($VM.Name). Retrying in 60 seconds..."
                    Start-Sleep -Seconds 60
                    $StartVM = Start-AzureRmVM -ResourceGroupName "$ResourceGroup" -Name $VM.Name -ErrorAction SilentlyContinue
                    $Attempt++
                }
                while(($StartVM.StatusCode) -ne 'OK' -and $Attempt -lt 5)
            }
           
            if($StartVM)
            {
                Write-Output -InputObject "Start-AzureRmVM cmdlet for $($VM.Name) with StatusCode:$($StartVM.StatusCode) on attempt number $Attempt of 5."
            }
        } 
    } 
}
#requires -Version 3 -Modules AzureRM.Compute, AzureRM.Profile
workflow Stop-Azure-VM
{
    param(
     
        [Parameter(Mandatory = $true)] 
        [string]$ResourceGroup,
        [Parameter(Mandatory = $true)] 
        [string]$SubscriptionCredential,
        [Parameter(Mandatory = $false)] 
        [string]$AzureSubscriptionID
    
    )
    # Stop the script if run in the weekend
    $CurrentDay = (Get-Date).DayOfWeek
    if ($CurrentDay -eq 'Saturday' -or $CurrentDay -eq 'Sunday')
    {
        Write-Output -InputObject 'Not a business day, canceling the procedure'
        exit
    }
    
    $Credential = Get-AutomationPSCredential -Name $SubscriptionCredential
    Add-AzureRmAccount -Credential $Credential

    $SubscriptionID = Get-AutomationVariable -Name $AzureSubscriptionID
    Select-AzureRmSubscription -SubscriptionId $SubscriptionID

    $VMs = Get-AzureRmVM -ResourceGroupName "$ResourceGroup" 
 
    # Stop VMs in parallel 
    if(!$VMs) 
    {
        Write-Output -InputObject 'No VMs were found in the specified Resource Group.'
    }
    else 
    {
        Foreach -parallel ($VM in $VMs) 
        {
            $StopVM = Stop-AzureRmVM -ResourceGroupName "$ResourceGroup" -Name $VM.Name -ErrorAction SilentlyContinue -Force
            
            $Attempt = 1
            if(($StopVM.StatusCode) -ne 'OK')
            {
                do
                {
                    Write-Output -InputObject "Failed to stop $($VM.Name). Retrying in 60 seconds..."
                    Start-Sleep -Seconds 60
                    $StopVM = Stop-AzureRmVM -ResourceGroupName "$ResourceGroup" -Name $VM.Name -ErrorAction SilentlyContinue
                    $Attempt++
                }
                while(($StopVM.StatusCode) -ne 'OK' -and $Attempt -lt 5)
            }
           
            if($StopVM)
            {
                Write-Output -InputObject "Stop-AzureRmVM cmdlet for $($VM.Name) with StatusCode:$($StopVM.StatusCode) on attempt number $Attempt of 5."
            }
        } 
    } 
}

Pin It on Pinterest