Using tags with Azure runbook automation to control your costs

I recently wrote an article here at TechGenix on how to start/stop VMs in Azure automation based on a schedule and life was good. I went to a customer this week that has a need to start and stop VMs, so I thought, Great! Copy and paste my previous article and Bob’s your uncle! Well, the conditions are more complex than my initial attempt. The goal of this article is to show how to address more complex requirements and apply them to your Azure environment using tags.

Here is a summary of their requirements. Basically, they have a group of servers that have to obey these following rules:

  • They are grouped into a single resource group.
  • They are a considerable number of servers (20+) that require a specific order to be turned on and turned off.
  • They have two tags to control the resource group, which are: PowerOnDays (a number for each day of the week, where Sunday is 1) and PowerOnHours, which is a range using a 24-hour format.
  • The developer team may need to test some new features. They want to change a tag to start the entire environment in a couple of hours, instead of leaving that to operations.

Based on the requirements, we are going to create an automation runbook. This runbook will run every hour, and based on the conditions will start taking action on the VMs.

Building a consistent array of servers

The first challenge in our script was to get the logic straight to create a list of servers. The customer threw me a curveball when he said that they had several environments and “sometimes” the names of the servers may have slight changes between environments. The script has to be dynamic and use a set of rules instead of static names.

After some discussion with the customer, I came up with the following list of servers and their proper order to be either started or shutdown. I highlighted the unique string of each server, so any changes to the prefix (which includes PROD in the example) and the number of servers should not impact the current automation process.

Azure automation tags

To address the current requirement, we created a functional called AddVMArray(), and I added the Startup Order above in the variable $vTiers. The script looks for a server with the string FS0 and if it finds, adds to the array, and if there is more than one server (for example AzProdFS01 and AZProdFS02), they will both be added to the array.

Function AddVMArray ($rg){
$vTiers = ‘*FS0*’,’*APP0*’,’*FE0*’,’*Sec0*’,’*RDS0*’
$fArray = @()
For ($i=0; $i -le (($vTiers).Count - 1); $i++){
#Debug: Write-Output $vtiers[$i]
$tVM = Get-AzVM -Name $vTiers[$i] -ResourceGroupName $rg
If ((($tVM).Count) -eq 0) {
$fArray += ‘none’
$tVM | ForEach{$fArray += $tVM.Name}
return $fArray

Bringing servers up and down

At this stage, we have a consistent list of the servers that are consistent with the servers in any given resource group. We know that going from 1 to 5, we have a list to start the servers correctly, and from 5 to 1, we have a list to stop the servers.

A function called VMGroup was created, the function receives two parameters: the action (on or off) and the resource group.

The function will go on every server of our array and either start or stop the given VM and add a delay defined in a variable between operations. This function will test the current state of the server before issuing a command to start and stop, this way we save some time.

Function VMGroup ($action, $ResourceGroup){
Write-Output "VMGroup Funtion --> " $ResourceGroup
Write-Output "VMGroup Funtion --> " $action
If (($action -eq $null) -or ($ResourceGroup -eq $null)) { Write-Output "Parameter Null in VMGroup Function"; break}
$vArray = @()
$vArray = AddVMArray($ResourceGroup)
$vNumberofServers= ($varray).count - 1
If ($action -eq "On") {
for ($i=0; $i -le $vNumberofServers; $i++){
if ((get-azvm -Name $varray[$i]) -eq $null) {
Write-Output "-> Status: VM " $varray[$i] " does not exist"
} Else {
If ((Get-AZVM -Name $varray[$i] -status).PowerState -ne "VM running") {
Start-AZVM -Name $varray[$i] -ResourceGroupName $ResourceGroup
Write-Output "-> Status: VM " $varray[$i] " is being started."
Start-Sleep -Seconds $StartupDelay
} Else {
Write-Output "-> Status: VM " $varray[$i] " is already running."
If ($action -eq "Off") {
for ($i=$vNumberofServers; $i -ge 0; $i=$i-1){
if ((get-azvm -Name $varray[$i]) -eq $null) {
Write-Output "-> Status: VM " $varray[$i] " does not exist"
} Else {
If ((Get-AZVM -Name $varray[$i] -status).PowerState -ne "VM deallocated") {
Stop-AZVM -Name $varray[$i] -ResourceGroupName $ResourceGroup -Force
Write-Output "-> Status: VM " $varray[$i] " is being deallocated."
Start-Sleep -Seconds $ShutdownDelay
} Else {
Write-Output "-> Status: VM " $varray[$i] " is already deallocated."

Putting all the pieces together

Using the functions that we created previously, we have enough tools to create a list of servers and perform start/stop operations on them.

The beauty of using functions is that all those instructions are reusable code, and can adapt based on the information provided.

The script itself is small, and there is a simple block where we define some initial variables.

$ShutdownDelay = 120
$StartupDelay = 300
$vCurrentDayofWeek = Get-Date -UFormat %u
$vCurrentHour = (Get-Date).Hour – 4

The next step is to narrow down as much as possible. We will use where-object to gather only resource groups that have a specific name, and we are looking for two particular tags. The tags are going to be the PowerOnHours and PowerOnDaysofWeek, the first must not be empty, and the second must match today’s day.

$gResourceGroups = Get-AzResourceGroup | where-object {($_.ResourceGroupName -like ‘*grpServer*’) -and ($_.Tags.PowerOnHours -ne $Null) -and ($_.Tags.PowerOnDaysOfWeek -like ("*" + $vCurrentDayofWeek + "*") }

Now that we have a list of all resource groups where we can take some action, we will perform a series of tests. The first one is to retrieve the tag PowerOnHours and compare against the current hour. If the current hour belongs to the interval, the script will turn on the VMs, if not then the VMs will be turned off.

$gResourceGroups | % {
Write-Output "==Working on " $_.ResourceGroupName " ====="
Write-Output "Working Days......:" $_.Tags.PowerOnDaysOfWeek
Write-Output "Hours of the Day..:" $_.Tags.PowerOnHours
$vHourTemp = $_.Tags.PowerOnHours.Split("-")
$vHourStart = $vHourTemp[0]
$vHourEnd = $vHourTemp[1]
If (($_.Tags.PowerOnDaysOfWeek -like $vCurrentDayofWeek) -and ( ($vCurrentHour -ge $vHourStart) -and ($vCurrentHour -le $vHourEnd))){
$vAction = "On"
$vAction = "Off"
Write-Output $vAction
Write-Output $_.ResourceGroupName
Write-Output $vEnv
Write-Output "Action: " $vaction
Write-Output "rg....: " $_.ResourceGroupName
VMGroup $vAction $_.ResourceGroupName

Wrapping it up

In this article, we went over a script that was designated to start and stop VMs based on tags associated at the resource group level. The script is dynamic and uses a list of unique strings to create a list of servers.

The entire runbook code can be found on GitHub here.

Featured image: Shutterstock

About The Author

Leave a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Scroll to Top