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.

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

Anderson Patricio

Anderson Patricio is a Canadian MVP in Cloud and Datacenter Management, and Office Server and Services, besides of the Microsoft Award he also holds a Solutions Master (MCSM) in Exchange, CISSP and several other certifications. Anderson contributes to the Microsoft Community with articles, tutorials, blog posts, twitter, forums and book reviews. He is a regular contributor here at,, and Anderson (Portuguese).

Published by
Anderson Patricio

Recent Posts

Docker, Microsoft unveil easier way to deploy Azure containers

Docker and Microsoft have rolled out a new and easier way for developers to deploy…

1 hour ago

Improvements on the verify domain error in Office 365

The verify domain error when registering the same domain in Office 365 to a different…

5 hours ago

Using VMM to run scripts to manage remote Hyper-V hosts

When it comes to the bulk management of Hyper-V hosts (or of any Windows server,…

8 hours ago

Shiny Hunters hacking group breach Home Chef database

The Shiny Hunters hacking group has struck again. This time they hit meal-prep delivery company…

1 day ago

Review: Specops uReset Active Directory self-service password reset

Specops uReset is an Active Directory password reset solution to handle the problem of forgotten…

1 day ago

Reports say eBay port scanning incoming visitors. Why?

According to several reports, eBay may be port scanning visitors to its site. While this…

4 days ago