Posts Automate Azure VM Start-Stop with Azure Automation and Tags
Post
Cancel

Automate Azure VM Start-Stop with Azure Automation and Tags

Automate Azure VM Start-Stop with Azure Automation and Azure Tags

One way to optimize the Microsoft Azure cloud cost is to properly deallocate and stop services when they are not in use. In case that we have a Virtual Machine that performs only specific tasks during a defined time frame, we can easily automate stopping the VM when it is not in use.

In this blog post, I will show you how you can automate the start and stop of Virtual Machines based on Azure Tag values. I’m not going to use Microsoft’s solution that is pre-created. Instead, I’m going to show you how to create it from scratch. You can check out existing Start-Stop VM solution from Microsoft HERE.

Preparation of environment

Create Azure Automation Account

If you don’t have an Automation Account, go ahead and create one. If you already have an Account that you can use for this purpose, you can skip this step.

Because we will be using Az PowerShell cmdlets, and Azure Automation Account comes with AzureRM by default, we also need to install some modules to it. Here is the list of modules I would suggest importing for now:

1
2
3
4
Az.Accounts
Az.Automation
Az.Compute
Az.Resources

If you want to stick with AzureRM modules instead, rewriting the Runbook script should be easy, since we are only using a few Az cmdlets, and they have the same equivavelent in AzureRM module.

You also need to give your Automation Account some access rights to perform this action. If you wish to create a new role, you only need these permissions:

1
2
3
4
5
"Microsoft.Compute/virtualMachines/deallocate/action"
"Microsoft.Compute/virtualMachines/read"
"Microsoft.Compute/virtualMachines/restart/action"
"Microsoft.Compute/virtualMachines/start/action"
"Microsoft.Compute/virtualMachineScaleSets/read"

Define and assign Azure Tags

Before we decide on the Tagging strategy for this task, we need to identify the information we need to store there.

  • We need to know if there are any specific times of the day and days of the week when VM should be available.

  • Since we will be working with the time, we need to define the time zone. The default time zone in Azure is UTC. If you need to work with a different time zone, you need to define this somewhere. Some environments spread across multiple regions and time zones, so this can’t be hardcoded in the Runbook. You can determine the time zone based on the Azure Region location, but we will include the UTC offset inside the Tag for more flexibility.

  • And finally, we need to be able to specify some exceptions. This is useful for maintenance windows, ignoring specific days (such as public holidays), ignoring action (such as Stop), or skipping days of the week, specific dates or weekends.

I often see environments where companies specify some of these values in Tags, such as Days and Hours operational. And it’s better to repurpose these Tags than to have the same values duplicated. So in this example, we will use multiple Tags to store these values rather than a single Tag. However, using a single Tag instead is much cleaner solution.

We will be using the following Tags:

Operational-Schedule
Operational-Weekdays
Operational-Weekends
Operational-UTCOffset
Operational-Exclusions

Here are the sample values:

Operational-Schedule: Yes
Any other Tag value or missing Tag will exclude this VM from the schedule.

Operational-Weekdays: 7-18
Contains the hours in 24h format when the VM should be operational during regular working days.

Operational-Weekends: 7-13
Contains the hours in 24h format when the VM should be operational during weekends. It also accepts value Off.

Operational-UTCOffset: -8
Plus or minus offset from UTC.

Operational-Exclusions: Sunday,Jan 01,Weekends
Exclusions from the schedule. Day of the week will exclude that day of the week every week. VM will stay on or off, depending on the last action from the previous run.

Accepted values for exceptions:
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
Weekdays
Weekends
Jan 10
Stop, Start

To assign these Tags to your VMs, I created a quick little PowerShell snippet that you can use for a single VM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#Specify VM names to which you want to add Tags:
$VMs = @("VM1", "VM2", "VM3")

foreach ($VMName in $VMs) {

    $VM = Get-AzResource -ResourceType "Microsoft.Compute/VirtualMachines" -Name "$VMName"

    $Tags = $VM.Tags
    $Tags += @{"Operational-Schedule"="Yes"}
    $Tags += @{"Operational-Weekdays"="7-18"}
    $Tags += @{"Operational-Weekends"="7-13"}
    $Tags += @{"Operational-UTCOffset"="-8"}
    $Tags += @{"Operational-Exclusions"="Weekends"}

    $VM | Set-AzResource -Tag $Tags -Force

}

Automation

Create Azure Automation Runbook

Now that we have our Automation Account and our Tags assigned, we need to create and schedule Azure Automation Runbook to perform the Start-Stop task.

To make things easier to understand, I will not be using built-in Variables, and we are not going to create a specific schedule with parameters. Everything we need is VM specific and already included in Tags. We can target this script to a larger scope, and we only need to create one schedule.

First, we need to connect and authenticate to Azure:


UPDATE: Azure Automation Account now supports Managed Identities. Check out THIS article on my blog to learn how to use System Assigned Managed Identity instead of Run As Account that is used in the following script.


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
Write-Output "Logging into Azure subscription using Az cmdlets..."
    
$connectionName = "AzureRunAsConnection"
try
{
    # Get the connection "AzureRunAsConnection "
    $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName         

    Add-AzAccount `
        -ServicePrincipal `
        -TenantId $servicePrincipalConnection.TenantId `
        -ApplicationId $servicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 
    
    Write-Output "Successfully logged into Azure subscription using Az cmdlets..."
}

catch {
    if (!$servicePrincipalConnection)
    {
        $ErrorMessage = "Connection $connectionName not found."
        throw $ErrorMessage
    } else{
        Write-Error -Message $_.Exception
        throw $_.Exception
    }
}

And here is the rest of our PoweShell Runbook:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#Get all VMs that should be part of the Schedule:
$VMs = Get-AzResource -ResourceType "Microsoft.Compute/VirtualMachines" -TagName "Operational-Schedule" -TagValue "Yes"

foreach ($VM in $VMs) {

    Write-Output "Processing VM $($VM.Name)..."

    ### Time Offset calculation

    #Get Current UTC Time (default time zone in all Azure regions)
    $UTCNow = [System.DateTime]::UtcNow

    #Get the Value of the "Operational-UTCOffset" Tag, that represents the offset from UTC
    $UTCOffset = $($VM.Tags)."Operational-UTCOffset"

    #Get current time in the Adjusted Time Zone
    if ($UTCOffset) {
        $TimeZoneAdjusted = $UTCNow.AddHours($UTCOffset)
        Write-Output "Current time of VM after adjusting the Time Zone is: $TimeZoneAdjusted"
    } else {
        $TimeZoneAdjusted = $UTCNow
    }


    ### Current Time associations

    $Day = $TimeZoneAdjusted.DayOfWeek

    If ($Day -like "S*") {
        $TodayIsWeekend = $true
        $TodayIsWeekday = $false

    } else {
        $TodayIsWeekend = $false
        $TodayIsWeekday = $true
    }

    
    ### Get Exclusions
    $Exclude = $false
    $Reason = ""
    $Exclusions = $($VM.Tags)."Operational-Exclusions"

    $Exclusions = $Exclusions.Split(',')
    
    foreach ($Exclusion in $Exclusions) {

        #Check excluded actions:
        If ($Exclusion.ToLower() -eq "stop") {$VMActionExcluded = "Stop"}
        If ($Exclusion.ToLower() -eq "start") {$VMActionExcluded = "Start"}
        
        #Check excluded days and compare with current day
        If ($Exclusion.ToLower() -like "*day") {
            if ($Exclusion -eq $Day) { $Exclude = $true; $Reason=$Day}
        }

        #Check excluded weekdays and copare with Today
        If ($Exclusion.ToLower() -eq "weekdays") {
                if ($TodayIsWeekday) {$Exclude = $true; $Reason="Weekday"}
        }

        #Check excluded weekends and compare with Today
        If ($Exclusion.ToLower() -eq "weekends") {
            if ($TodayIsWeekend) {$Exclude = $true; $Reason="Weekend"}
        }

        If ($Exclusion -eq (Get-Date -UFormat "%b %d")) {
            $Exclude = $true; $Reason = "Date Excluded"
        }

    }

    if (!$Exclude) {

        #Get values from Tags and compare to the current time

        if ($TodayIsWeekday) {

            $ScheduledTime = $($VM.Tags)."Operational-Weekdays"
        
        } elseif ($TodayIsWeekend) {

            $ScheduledTime = $($VM.Tags)."Operational-Weekends"

        }

        if ($ScheduledTime) {
            
            $ScheduledTime = $ScheduledTime.Split("-")
            $ScheduledStart = $ScheduledTime[0]
            $ScheduledStop = $ScheduledTime[1]
            
            $ScheduledStartTime = Get-Date -Hour $ScheduledStart -Minute 0 -Second 0
            $ScheduledStopTime = Get-Date -Hour $ScheduledStop -Minute 0 -Second 0

            If (($TimeZoneAdjusted -gt $ScheduledStartTime) -and ($TimeZoneAdjusted -lt $ScheduledStopTime)) {
                #Current time is within the interval
                Write-Output "VM should be running now"
                $VMAction = "Start"
            
            } else {
                #Current time is outside of the operational interval
                Write-Output "VM should be stopped now"
                $VMAction = "Stop"

            }

            If ($VMAction -notlike "$VMActionExcluded") { #Make sure that action was not excluded

                #Get VM PowerState status
                $VMObject = Get-AzVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -Status
                $VMState = ($VMObject.Statuses | Where-Object Code -like "*PowerState*").DisplayStatus
                
                if (($VMAction -eq "Start") -and ($VMState -notlike "*running")) {

                    Write-Output "Starting $($VM.Name)..."
                    Start-AzVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name


                } elseif (($VMAction -eq "Stop") -and ($VMState -notlike "*deallocated")) {
                    
                    Write-Output "Stopping $($VM.Name)..."
                    Stop-AzVM -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -Force

                } else {

                    Write-Output "VM $($VM.Name) status is: $VMState . No action will be performed ..."

                }

                
            } else {
                Write-Output "VM $($VM.Name) is Excluded from changes during this run because Operational-Exclusions Tag contains action $VMAction."

            }


        } else {

            Write-Output "Error: Scheduled Running Time for VM was not detected. No action will be performed..."
        }
        

    } else {

        Write-Output "VM $($VM.Name) is Excluded from changes during this run because Operational-Exclusions Tag contains exclusion $Reason."
    }

}

Write-Output "Runbook completed."

Configure running Runbook on a schedule

One last thing to do is to schedule the execution of our new Azure Automation Runbook.

For this purpose I created new hourly schedule, that will run at 15 minutes past hour. And I linked it to the newly created Runbook.

You can also create something more specific, for example running it once in the morning after Scheduled Start Time, and then once in the evening after Scheduled Stop Time. Just make sure you select the correct time zone while creating your schedule.

Azure Automation Schedule

To be considered before running in production environment

The Runbook example provided here is very simple. Before using this solution in the Production environment, here are few things to consider improving:

  • Error tracking and exception handling needs to be improved. Also, incorrect data in Tag values is not expected, but in the real world it needs to be.
  • This example doesn’t include any logging and notifications. Azure Automation Account can work nicely with Azure Monitor and Log Analytics. Consider implementing that.
  • In more complicated cases where we need to work with Multi-Layer applications, we also need to know the sequence in which to bring VMs up or down, and we might need to run some pre or post-action tasks.
  • Performing shutdown outside of maintenance window can also trigger alerts in traditional monitoring solutions, such as SCOM. So we need to think about configuring that as well.
  • Consider using one Azure Tag instead of many. Having multiple Tags with long names just for this purpose is not very clean solution. These Tags with long names can make other important Tags less noticeable.

Thank you for reading. Keep clouding around.

Vukašin Terzić

Updated Mar 29, 2022 2022-03-29T21:29:51+02:00
This post is licensed under CC BY 4.0