Blog Post

Core Infrastructure and Security Blog
5 MIN READ

Switch off Virtual Machines on a schedule using an Azure Tag

wernerrall's avatar
wernerrall
Icon for Microsoft rankMicrosoft
Aug 23, 2024

1. Introduction

Managing the costs associated with running virtual machines (VMs) in Azure can be challenging, especially when VMs are left running during non-business hours. One effective solution is to schedule automatic shutdowns using Azure tags. In this blog, we'll walk you through a PowerShell script that uses an Azure tag to define and enforce VM shutdown schedules.

 

2. Prerequisites

Before you get started, ensure you have the following:

  • An active Azure subscription.
  • Appropriate permissions to manage VMs and read tag information.
  • Basic knowledge of PowerShell scripting.

 

3. Script Overview

The script manages the power state of Azure virtual machines based on a specified tag and its value, which defines the schedule. For example, a tag named "AutoShutdownSchedule" with a value of "10PM -> 6AM" will shut down the VM at 10 PM and start it at 6 AM.

 

4. Detailed Script Breakdown

Parameters and Initial Setup

The script accepts three parameters:

  • TagName: The name of the tag to look for on virtual machines.
  • ManagementGroupId: The ID of the Azure management group to operate on.
  • Simulate: If set to $true, the script will only simulate the actions without making any changes.
param (
    [parameter(Mandatory = $true)]
    [string]$TagName,
 
    [parameter(Mandatory = $true)]
    [string]$ManagementGroupId,
 
    [parameter(Mandatory = $false)]
    [bool]$Simulate = $false
)
 
Function: Get-SubscriptionsUnderManagementGroup

This function retrieves all subscription IDs under a specified Azure management group.

 

function Get-SubscriptionsUnderManagementGroup {
    param (
        [Parameter(Mandatory = $true)]
        [string]$ManagementGroupId
    )

    # Array to store subscription IDs
    $subscriptionIds = @()

    # Get the management group hierarchy
    $managementGroup = Get-AzManagementGroup -GroupId $ManagementGroupId -Expand

    if ($managementGroup -and $managementGroup.Children) {       
        # Loop through each child in the management group
        foreach ($child in $managementGroup.Children) {
            if ($child.Type -eq "Microsoft.Management/managementGroups") {
                # Recursively get subscriptions from child management groups
                $childManagementGroupId = $child.Name
                $subscriptionIds += Get-SubscriptionsUnderManagementGroup -ManagementGroupId $childManagementGroupId
            } elseif ($child.Type -match "/subscriptions") {
                # Extract subscription ID
                $subscriptionId = [regex]::Match($child.Name, "([a-f0-9-]{36})").Value
                if ($subscriptionId) {
                    $subscriptionIds += $subscriptionId
                }
            }
        }
    }

    return $subscriptionIds
}

 

Function: CheckScheduleEntry

This function checks if the current time falls within a specified time range.

 

function CheckScheduleEntry ([string]$TimeRange) {  
    $rangeStart, $rangeEnd, $parsedDay = $null
    $currentTime = (Get-Date).ToUniversalTime().AddHours(2)
    $midnight = $currentTime.AddDays(1).Date
 
    try {
        if ($TimeRange -like "*->*") {
            $timeRangeComponents = $TimeRange -split "->" | ForEach-Object { $_.Trim() }
            if ($timeRangeComponents.Count -eq 2) {
                $rangeStart = Get-Date $timeRangeComponents[0]
                $rangeEnd = Get-Date $timeRangeComponents[1]
 
                if ($rangeStart -gt $rangeEnd) {
                    if ($currentTime -ge $rangeStart -and $currentTime -lt $midnight) {
                        $rangeEnd = $rangeEnd.AddDays(1)
                    }
                    else {
                        $rangeStart = $rangeStart.AddDays(-1)
                    }
                }
            }
            else {
                Write-Output "`WARNING: Invalid time range format. Expects valid .Net DateTime-formatted start time and end time separated by '->'" 
            }
        }
        else {
            if ([System.DayOfWeek].GetEnumValues() -contains $TimeRange) {
                if ($TimeRange -eq (Get-Date).DayOfWeek) {
                    $parsedDay = Get-Date "00:00"
                }
            }
            else {
                $parsedDay = Get-Date $TimeRange
            }
 
            if ($parsedDay -ne $null) {
                $rangeStart = $parsedDay
                $rangeEnd = $parsedDay.AddHours(23).AddMinutes(59).AddSeconds(59)
            }
        }
    }
    catch {
        Write-Output "`WARNING: Exception encountered while parsing time range. Details: $($_.Exception.Message). Check the syntax of entry, e.g. '<StartTime> -> <EndTime>', or days/dates like 'Sunday' and 'December 25'"   
        return $false
    }
 
    if ($currentTime -ge $rangeStart -and $currentTime -le $rangeEnd) {
        return $true
    }
    else {
        return $false
    }
}

 

 

Function: AssertVirtualMachinePowerState

This function ensures that a VM is in the desired power state (running or stopped) based on the schedule.

 

 

function AssertVirtualMachinePowerState {
    param (
        [Object]$VirtualMachine,
        [string]$DesiredState,
        [bool]$Simulate
    )
 
    $resourceManagerVM = Get-AzVM -ResourceGroupName $VirtualMachine.ResourceGroupName -Name $VirtualMachine.Name -Status
    $currentStatus = $resourceManagerVM.Statuses | Where-Object { $_.Code -like "PowerState*" }
    $currentStatus = $currentStatus.Code -replace "PowerState/", ""
 
    if ($DesiredState -eq "Started" -and $currentStatus -notmatch "running") {
        if ($Simulate) {
            Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have started VM. (No action taken)"
        }
        else {
            Write-Output "[$($VirtualMachine.Name)]: Starting VM"
            $resourceManagerVM | Start-AzVM
        }
    }
    elseif ($DesiredState -eq "StoppedDeallocated" -and $currentStatus -ne "deallocated") {
        if ($Simulate) {
            Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have stopped VM. (No action taken)"
        }
        else {
            Write-Output "[$($VirtualMachine.Name)]: Stopping VM"
            $resourceManagerVM | Stop-AzVM -Force
        }
    }
    else {
        Write-Output "[$($VirtualMachine.Name)]: Current power state [$currentStatus] is correct."
    }
}

 

 

The script iterates through all VMs in the specified subscriptions, checks their tags, and enforces the power state according to the schedule. Below is the outline of what it does

 

try {
    # Main script logic
}
catch {
    $errorMessage = $_.Exception.Message
    throw "Unexpected exception: $errorMessage"
}
finally {
    Write-Output "Script finished (Duration: $(("{0:hh\:mm\:ss}" -f ((Get-Date).ToUniversalTime() - $currentTime))))"
}

 

5. Usage Example

To run the script, use the following command:

.\StartStopVMsBasedOnTag.ps1 -TagName "AutoShutdownSchedule" -ManagementGroupId "MngEnv" -Simulate $true

I will be running the example in Local PowerShell, but the PowerShell could be run from anywhere including Automation Accounts. 

 

5.1 We tag our virtual machine accordingly

 

 

5.2 We look at our PowerShell code after being signed in to Azure and we run our command

 

 

5.3 We used the "-Simulate $true" flag which shows us what would have happened. If we want to run this in production, we can simulate first and when we are happy with our testing we can stop simulating by switching the "-Simulate $false"

 

 

6. Conclusion

Automating VM shutdown schedules using Azure tags helps optimize resource usage and reduce costs. By following this guide, you can implement a similar solution in your Azure environment. If you have any questions or feedback, feel free to leave a comment below. You can find a copy of this code in my GitHub Repo --> RallTheory/StartStopVMs/StartStopVMsBasedOnTag.ps1 at main ยท WernerRall147/RallTheory (github.com)

 

Disclaimer

The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts or Power BI Dashboards are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts or Power BI Dashboards be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages. This blog post was written with the help of generative AI. 

Updated Aug 01, 2024
Version 1.0
  • masih-shek's avatar
    masih-shek
    Copper Contributor

    Hello, 

     

    is there a way to limit the virtual machine usage using, function etc? We want to give the users some limited amount of hours for using their VMs. Once that limit is reached the VM shouid shut down. Similar to something that is used in Azure Lab Services. Thank you!