Jump to content

Switch off Virtual Machines on a schedule using an Azure Tag


Recommended Posts

Guest wernerrall
Posted

[HEADING=3]1. Introduction[/HEADING]

 

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.

 

 

 

[HEADING=3]2. Prerequisites[/HEADING]

 

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.

 

 

 

[HEADING=3]3. Script Overview[/HEADING]

 

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.

 

 

 

[HEADING=3]4. Detailed Script Breakdown[/HEADING]

[HEADING=3]Parameters and Initial Setup[/HEADING]

 

The script accepts three parameters:

 

  • [iCODE]TagName[/iCODE]: The name of the tag to look for on virtual machines.
  • [iCODE]ManagementGroupId[/iCODE]: The ID of the Azure management group to operate on.
  • [iCODE]Simulate[/iCODE]: If set to [iCODE]$true[/iCODE], 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
)

[HEADING=3] [/HEADING]

[HEADING=3]Function: Get-SubscriptionsUnderManagementGroup[/HEADING]

 

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
}

 

 

 

[HEADING=3]Function: CheckScheduleEntry[/HEADING]

 

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
   }
}

 

 

 

 

 

[HEADING=3]Function: AssertVirtualMachinePowerState[/HEADING]

 

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))))"
}

 

 

 

[HEADING=3]5. Usage Example[/HEADING]

 

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

 

 

 

[ATTACH type=full" alt="wernerrall_0-1722513240849.png]63761[/ATTACH]

 

 

 

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

 

 

 

[ATTACH type=full" alt="wernerrall_1-1722513686491.png]63762[/ATTACH]

 

 

 

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"

 

 

 

[ATTACH type=full" alt="wernerrall_2-1722513896428.png]63763[/ATTACH]

 

 

 

[HEADING=3]6. Conclusion[/HEADING]

 

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.

 

Continue reading...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...