Scheduled Virtual Machine Shutdown/Startup - Microsoft Azure
Save on your bill by automating the scheduled power management of your Microsoft Azure virtual machines
Solution Guide
Hi there! We're humbled to have seen thousands of downloads of this runbook since its initial release. By popular demand and based on many feature requests, we've developed a fully-featured app for scheduling downtime for your Azure virtual machines. It's called SkyPlug, and it offers many benefits beyond what you'll find in this runbook including:
- More flexible scheduling in any time zone
- Targeted email notifications
- Simple code-free setup
- Team accounts
- Detailed logs
- ...and more
SkyPlug is available in several plans, including a free personal edition.
Learn more:
What It Does
This runbook automates scheduled startup and shutdown of Azure virtual machines. You can implement multiple granular power schedules for your virtual machines using simple tag metadata in the Azure portal or through PowerShell. For example, you could tag a single VM or group of VMs to be shut down between the hours of 10:00 PM and 6:00 AM, all day on Saturdays and Sundays, and during specific days of the year, like December 25.
The runbook is intended to run on a schedule in an Azure Automation account, with a configured subscription and associated access credentials. For example, it can run once every hour, checking all the schedule tags it finds on your virtual machines or resource groups. If the current time falls within a shutdown period you’ve defined, the runbook will stop the VM if it is running, preventing any compute charges. If the current time falls outside of any tagged shutdown period, this means the VM should be running, so the runbook starts any such VM that is stopped.
Once the runbook is in place and scheduled, the only configuration required can be done through simple tagging of resources, and the runbook will implement whatever power schedules it finds during its next scheduled run. Think of this as a quick and basic power management scheduling solution for your Azure virtual machines.
Sound enticing? Read on for more details.
Why Use This?
Money! The largest share of Azure subscription costs when using Virtual Machines (IaaS) is the compute time: how many hours the VMs are running per month. If you have VMs that can be stopped during certain time periods, you can reduce the bill by turning them off (and “deallocating” them).
Unfortunately, Microsoft doesn’t include any tools to directly manage a schedule like this. That’s what this runbook helps achieve without 3rd party management tools or chaining a junior admin to the keyboard for 6AM wakeup call.
How it Works
Let’s get into more detail about how the runbook makes the magic happen.
Tag-based Power Schedules
If our goal is to manage the times that our virtual machines are shut down and powered on, we need a way to define this schedule. For example, perhaps we want to shut down our VMs after close of business and have them start up before people arrive in the office in the morning. But we also might want them shut down all weekend, not just at night. And what about holidays? Clearly, we also need an approach that allows some flexibility to get granular with scheduling.
The first thing we might think to use is a runbook schedule, which Azure already provides out of the box. In essence, we can configure a runbook to run hourly or daily and do a task like shutting down VMs. But as just discussed, what if you have multiple schedules for different VMs? And that’s for shutting down – what about starting them again? Do you use multiple runbooks following multiple schedules? This starts to get confusing and awkward to manage. Unfortunately most of the existing examples I came across followed this kind of approach.
When you think about it, the power schedule applies to the resource, not to the runbook. The alternative approach used by the runbook solution here described is to tag a VM with a schedule, so that the mechanism used to stop and start VMs is transparent – it just happens when you declare that it should. If you’re especially nerdy when it comes to programming, you might recognize this as a declarative rather than imperative approach. It doesn’t use PowerShell Desired State Configuration (yet?), but is in the same spirit.
So what does it look like? We simply apply a tag to a virtual machine or an Azure resource group that contains VMs. This tag is a simple string that describes the times the VM should be shut down.
Tag Content
The runbook looks for a tag named “AutoShutdownSchedule” assigned to a virtual machine or resource group containing VMs. The value of this tag is one or more schedule entries, or time ranges, defining when VMs should be shut down. By implication, any times not defined in the shutdown schedule are times the VM should be online. So, each time the runbook checks the current time against the schedule, it makes sure the VM is powered on or off accordingly.
There are two kinds of entries:
-
Time range: two times of day or absolute dates separated by ‘->’ (dash greater-than). Use this to define a period of time when VMs should be shut down.
-
Day of week / Date: Interpreted as a full day that VMs should be shut down.
Get to Know DateTime
All times must be strings that can be successfully parsed as “DateTime” values. In other words, PowerShell must be able to look at the text value you provide and say “OK, I know how to interpret that as a specific date/time”. There is a surprising amount of flexibility allowed, and the easiest way to verify your choice is to open a PowerShell prompt and try the command Get-Date “<time text>”, and see what happens. If PowerShell spits out a formatted timestamp, that’s good. If it complains that it doesn’t know what you mean, try writing the time differently.
UTC Timezone
To simplify the code, the runbook expects all times to be in UTC, not local time. This means that before you define a schedule, first convert the times to UTC / GMT.
Schedule Tag Examples
The easiest way to write the schedule is to say it first in words as a list of times the VM should be shut down, then translate that to the string equivalent. Remember, any time period not defined as a shutdown time is online time, so the runbook will start the VMs accordingly. Let’s look at some examples:
Description |
Tag value |
Shut down from 10PM to 6 AM UTC every day |
10pm -> 6am |
Shut down from 10PM to 6 AM UTC every day (different format, same result as above) |
22:00 -> 06:00 |
Shut down from 8PM to 12AM and from 2AM to 7AM UTC every day (bringing online from 12-2AM for maintenance in between) |
8PM -> 12AM, 2AM -> 7AM |
Shut down all day Saturday and Sunday (midnight to midnight) |
Saturday, Sunday |
Shut down from 2AM to 7AM UTC every day and all day on weekends |
2:00 -> 7:00, Saturday, Sunday |
Shut down on Christmas Day and New Year’s Day |
December 25, January 1 |
Shut down from 2AM to 7AM UTC every day, and all day on weekends, and on Christmas Day |
2:00 -> 7:00, Saturday, Sunday, December 25 |
Shut down always – I don’t want this VM online, ever |
0:00 -> 23:59:59 |
What the Runbook Does
The runbook Assert-AutoShutdownSchedule is an Azure Automation runbook. It can be run once at a time manually, but is intended to be configured to run on a schedule, e.g. once per hour.
The runbook expects two parameters: the name of the Azure subscription that contains the VMs, and the name of an Azure Automation credential asset with stored username and password for the account to use when connecting to that subscription. If not specifically configured, the runbook will try by default to find a credential asset named “Default Automation Credential” and a variable asset named “Default Azure Subscription”. Setting these up is discussed in more detail below. There is a third parameter called "Simulate" which, if True, tells the runbook to only evaluate schedules but not enforce them. This is discussed further below.
Once successfully authenticated to the target subscription, the runbook looks for any VM or resource group that has a tag named “AutoShutdownSchedule”. Any resource groups without this specific tag are ignored. For each tagged resource found, we next look at the tag values to see what the schedule entries are. Each is inspected and compared with the current time. Then, one of several decisions is made:
-
If the current time is outside of the defined schedules, the runbook concludes that this is “online time” and starts any directly or indirectly tagged VM that is currently powered off.
-
If the current time matches any of the schedules, the runbook concludes that this is “shutdown time” and stops any directly or indirectly tagged VM that is currently powered on.
-
If any of the defined schedules can’t be parsed (PowerShell doesn’t understand “beer thirty”), it will ignore that and treat whatever was intended as online time. Therefore, the default failsafe behavior is to keep VMs online or start them, not shut them down.
Runbook Logs
Various output messages are recorded by the runbook every time it runs, indicating what actions were taken and whether any errors occurred in processing tags or accessing the subscription. These logs can be found in the output of each job.
Performance
Startup and shutdown are done sequentially, one VM at a time. This helps to avoid problems with VMs that belong to the same cloud service, which allow only one member VM at a time to power on or off. Starting and stopping can take a minute or two per VM, so if you have a large environment, it could be worthwhile to customize the runbook to perform parallel actions where possible to minimize job duration.
Testing
To test the runbook without actually starting or stopping your VMs, you can use the "Simulate" option. If true, the schedules will be evaluated but no power actions will be taken. You can then see whether everything would have worked as you expect before setting up the runbook to run live (runbook runs live by default).
Azure Modules
As of version 2.0 of the runbook, there are no other requirements for modules other than what is included in your Azure Automation account by default. This includes the "Azure" and "AzureRM.Resources" modules. If you don't know what modules are, don't worry. If you have removed either of these or they aren't present, make sure to address that.
Setting it up in Azure
Now we’ll go through the steps to get this working in your subscription. It will be beer thirty before you know it.
Prerequisites
This is an Azure Automation runbook, and as such you’ll need the following to use it:
-
Microsoft Azure subscription (including trial subscriptions)
-
Azure Automation account created in subscription (instructions)
-
Runbook file downloaded from this page or imported from runbook gallery
Import Runbook
The runbook is contained in the file “Assert-AutoShutdownSchedule.ps1” within the download. You can import this into your Automation Account like so:
-
Open subscription in https://portal.azure.com
-
Open the Automation Account which will contain the runbook
-
Open the Runbooks view from the Resources section
-
Click Add a runbook from the top menu
-
Select Import an existing runbook
-
Click Create to upload
-
Confirm “Assert-AutoShutdownSchedule” now appears in the runbooks list
-
Open the runbook from the list
-
Click Edit from the top menu
-
Click Publish from the top menu and confirm
-
Confirm the runbook now shows a status of Published
Create Credential Asset
When the runbook executes, it accesses your subscription with credentials you configure. By default, it looks for a credential named “Default Automation Credential”. This is for a user you create in your subscription’s Azure Active Directory which is granted permissions to manage subscription resources, e.g. as a co-administrator. The steps:
-
Create an Azure Active Directory user for runbook use if you haven’t already. This account will be the "service account" for the runbook and must be a co-administrator in the target subscription.
-
Open subscription in https://portal.azure.com
-
Open the Automation Account which will contain the runbook
-
Open the Assets view from the resources section
-
Open the Credentials view
-
Click Add a credential from the top menu
-
Enter details for the new credential. Recommended to use name “Default Automation Credential”.
-
Click Create
Create Variable for Subscription Name
The runbook also needs to know which subscription to connect to when it runs. In theory, a runbook can connect to any subscription, so we must specify one in particular. This is easily done by setting up a variable in our automation account.
-
Open subscription in https://portal.azure.com
-
Note your target subscription name as shown in Browse > Subscriptions
-
Open the Automation Account which will contain the runbook
-
Open the Assets view from the resources section
-
Open the Variables view
-
Click Add a variable from the top menu
-
Give the variable a name (“Default Azure Subscription” expected by default), and enter the subscription name as the variable’s value. Click Create.
Schedule the Runbook
The runbook should be scheduled to run periodically. As previously discussed, this does not determine the power on/power off schedule. It only determines how often the power schedules on resources are checked. Azure allows up to an hourly frequency, so we’ll take advantage of that:
-
Back in the runbooks list, open the new runbook “Asset-AutoShutdownSchedule”
-
Open the Schedules view under details
-
Click Add a schedule in top menu
-
Click Link a schedule to your runbook
-
Click Create a new schedule
-
Provide a name like “Hourly Runbook Schedule”
-
Set the start time to time you want to first run, e.g. the next upcoming hour mark
-
Set Recurrence to Hourly
-
Click Create
-
(Optional) If you want to provide a credential or subscription name directly and didn’t use the default names, click Configure your runbook parameters
-
(Optional) Enter the name of the credential asset the runbook should use
-
(Optional) Enter the name of the subscription the runbook should use
-
Click OK to close the open dialogs
-
Confirm the schedule now appears in the list with status Enabled
The runbook will now run every hour and perform power actions as indicated by the tags on resource groups in your subscription.
Configure Shutdown Schedule Tags
Finally, we need to tag our VM resource groups. The tag format was discussed above. To create schedule tags:
-
Open subscription in https://portal.azure.com
-
Navigate to Browse > Resource Groups, and open a resource group that contains VMs to schedule
-
Click the tag icon in the upper right
-
In the Key field, enter “AutoShutdownSchedule”
-
In the Value field, enter a schedule as discussed above, such as “10PM -> 6AM”
-
Click Save in the top menu
After repeating this process for each VM resource group in your subscription, everything is set to automatically shut down and start up your virtual machines. Going forward, you can simply update the tag as needed to adjust the schedule, and add a tag to new resource groups that require a shutdown schedule. Remember, VMs in untagged resource groups will not be managed by the runbook.
Initial Testing
To validate that the runbook works, we can run an initial test manually and inspect the results. This is easy:
-
Assign a shutdown schedule tag to the VM or resource group you want to use for testing. Give it a schedule the covers the current time. The easiest way is to just use today’s day of the week, e.g. “Wednesday”.
-
Start the test VM(s)
-
In the runbook view under your automation account, click the Start button from the top menu.
-
Verify the parameters are correct if you opted not to use the defaults. Set Simulation to True in order to test without making changes. Verify Run on Azure is selected and click OK
-
Open the Output view, and wait for the runbook to execute. It takes a minute or two to queue and run.
At this point, we hope to see messages in the output telling us a tagged VM or resource group was found, that the current time is within a shutdown schedule, and that the intended VMs would have been stopped in a normal execution. Any errors that occur should also be recorded in the output.
Now, test the opposite case: starting VMs that should be running according to the schedule (if they aren’t in an explicitly-defined shutdown period, they should be started). So, we can update our schedule tag and test again as follows:
-
Go back to the test VM or resource group and set the AutoShutdownSchedule such that it doesn’t cover the current time. For example, if today is Wednesday, set the tag value to “Tuesday”. Setting the tag again and saving overwrites any existing tag with the same name. (Hint: you can use the dropdown to select previous tag keys and values).
-
Now start the runbook again using the same steps as before and watch the output
This time, we should see that the current time doesn’t match any shutdown schedules for the VM or group, and see the runbook report that it would have started the intended VMs.
Troubleshooting
To check for problems, you can inspect the runbook job history to look at the output and streams / history for each individual job. In the new portal, the output view doesn't necessarily show error details, so make sure to check the Streams view as well.
Automation Account Configuration
Before putting this runbook into production where you count on it to reliably manage your VM power state, I recommend configuring your automation account as a “Basic” rather than a free account. This ensures that the 500 minute monthly run time limitation will not be hit and prevent the runbooks from working. The cost is extremely low for additional minutes, so the few extra dollars, if any, will easily be offset by the compute time savings.
This can be changed in the “Pricing Tier” view under the automation account in the portal at https://portal.azure.com.
Taking it Further
Please share your experiences with this runbook in the comments below, and if you have ideas for making it better, don’t be shy!
Acknowledgements
Some other helpful resources on the subject worth checking out:
Hands Free VM Management with Azure Automation and Resource Manager
Using Azure Automation to Start and Stop Virtual Machines on a Schedule
Script
<# .SYNOPSIS This Azure Automation runbook automates the scheduled shutdown and startup of virtual machines in an Azure subscription. .DESCRIPTION The runbook implements a solution for scheduled power management of Azure virtual machines in combination with tags on virtual machines or resource groups which define a shutdown schedule. Each time it runs, the runbook looks for all virtual machines or resource groups with a tag named "AutoShutdownSchedule" having a value defining the schedule, e.g. "10PM -> 6AM". It then checks the current time against each schedule entry, ensuring that VMs with tags or in tagged groups are shut down or started to conform to the defined schedule. This is a PowerShell runbook, as opposed to a PowerShell Workflow runbook. This runbook requires the "Azure" and "AzureRM.Resources" modules which are present by default in Azure Automation accounts. For detailed documentation and instructions, see: https://automys.com/library/asset/scheduled-virtual-machine-shutdown-startup-microsoft-azure .PARAMETER AzureCredentialName The name of the PowerShell credential asset in the Automation account that contains username and password for the account used to connect to target Azure subscription. This user must be configured as co-administrator and owner of the subscription for best functionality. By default, the runbook will use the credential with name "Default Automation Credential" For for details on credential configuration, see: http://azure.microsoft.com/blog/2014/08/27/azure-automation-authenticating-to-azure-using-azure-active-directory/ .PARAMETER AzureSubscriptionName The name or ID of Azure subscription in which the resources will be created. By default, the runbook will use the value defined in the Variable setting named "Default Azure Subscription" .PARAMETER Simulate If $true, the runbook will not perform any power actions and will only simulate evaluating the tagged schedules. Use this to test your runbook to see what it will do when run normally (Simulate = $false). .EXAMPLE For testing examples, see the documentation at: https://automys.com/library/asset/scheduled-virtual-machine-shutdown-startup-microsoft-azure .INPUTS None. .OUTPUTS Human-readable informational and error messages produced during the job. Not intended to be consumed by another runbook. #> param( [parameter(Mandatory=$false)] [String] $AzureCredentialName = "Use *Default Automation Credential* Asset", [parameter(Mandatory=$false)] [String] $AzureSubscriptionName = "Use *Default Azure Subscription* Variable Value", [parameter(Mandatory=$false)] [bool]$Simulate = $false ) $VERSION = "2.0.2" # Define function to check current time against specified range function CheckScheduleEntry ([string]$TimeRange) { # Initialize variables $rangeStart, $rangeEnd, $parsedDay = $null $currentTime = (Get-Date).ToUniversalTime() $midnight = $currentTime.AddDays(1).Date try { # Parse as range if contains '->' if($TimeRange -like "*->*") { $timeRangeComponents = $TimeRange -split "->" | foreach {$_.Trim()} if($timeRangeComponents.Count -eq 2) { $rangeStart = Get-Date $timeRangeComponents[0] $rangeEnd = Get-Date $timeRangeComponents[1] # Check for crossing midnight if($rangeStart -gt $rangeEnd) { # If current time is between the start of range and midnight tonight, interpret start time as earlier today and end time as tomorrow if($currentTime -ge $rangeStart -and $currentTime -lt $midnight) { $rangeEnd = $rangeEnd.AddDays(1) } # Otherwise interpret start time as yesterday and end time as today else { $rangeStart = $rangeStart.AddDays(-1) } } } else { Write-Output "`tWARNING: Invalid time range format. Expects valid .Net DateTime-formatted start time and end time separated by '->'" } } # Otherwise attempt to parse as a full day entry, e.g. 'Monday' or 'December 25' else { # If specified as day of week, check if today if([System.DayOfWeek].GetEnumValues() -contains $TimeRange) { if($TimeRange -eq (Get-Date).DayOfWeek) { $parsedDay = Get-Date "00:00" } else { # Skip detected day of week that isn't today } } # Otherwise attempt to parse as a date, e.g. 'December 25' else { $parsedDay = Get-Date $TimeRange } if($parsedDay -ne $null) { $rangeStart = $parsedDay # Defaults to midnight $rangeEnd = $parsedDay.AddHours(23).AddMinutes(59).AddSeconds(59) # End of the same day } } } catch { # Record any errors and return false by default Write-Output "`tWARNING: 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 } # Check if current time falls within range if($currentTime -ge $rangeStart -and $currentTime -le $rangeEnd) { return $true } else { return $false } } # End function CheckScheduleEntry # Function to handle power state assertion for both classic and resource manager VMs function AssertVirtualMachinePowerState { param( [Object]$VirtualMachine, [string]$DesiredState, [Object[]]$ResourceManagerVMList, [Object[]]$ClassicVMList, [bool]$Simulate ) # Get VM depending on type if($VirtualMachine.ResourceType -eq "Microsoft.ClassicCompute/virtualMachines") { $classicVM = $ClassicVMList | where Name -eq $VirtualMachine.Name AssertClassicVirtualMachinePowerState -VirtualMachine $classicVM -DesiredState $DesiredState -Simulate $Simulate } elseif($VirtualMachine.ResourceType -eq "Microsoft.Compute/virtualMachines") { $resourceManagerVM = $ResourceManagerVMList | where Name -eq $VirtualMachine.Name AssertResourceManagerVirtualMachinePowerState -VirtualMachine $resourceManagerVM -DesiredState $DesiredState -Simulate $Simulate } else { Write-Output "VM type not recognized: [$($VirtualMachine.ResourceType)]. Skipping." } } # Function to handle power state assertion for classic VM function AssertClassicVirtualMachinePowerState { param( [Object]$VirtualMachine, [string]$DesiredState, [bool]$Simulate ) # If should be started and isn't, start VM if($DesiredState -eq "Started" -and $VirtualMachine.PowerState -notmatch "Started|Starting") { if($Simulate) { Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have started VM. (No action taken)" } else { Write-Output "[$($VirtualMachine.Name)]: Starting VM" $VirtualMachine | Start-AzureVM } } # If should be stopped and isn't, stop VM elseif($DesiredState -eq "StoppedDeallocated" -and $VirtualMachine.PowerState -ne "Stopped") { if($Simulate) { Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have stopped VM. (No action taken)" } else { Write-Output "[$($VirtualMachine.Name)]: Stopping VM" $VirtualMachine | Stop-AzureVM -Force } } # Otherwise, current power state is correct else { Write-Output "[$($VirtualMachine.Name)]: Current power state [$($VirtualMachine.PowerState)] is correct." } } # Function to handle power state assertion for resource manager VM function AssertResourceManagerVirtualMachinePowerState { param( [Object]$VirtualMachine, [string]$DesiredState, [bool]$Simulate ) # Get VM with current status $resourceManagerVM = Get-AzureRmVM -ResourceGroupName $VirtualMachine.ResourceGroupName -Name $VirtualMachine.Name -Status $currentStatus = $resourceManagerVM.Statuses | where Code -like "PowerState*" $currentStatus = $currentStatus.Code -replace "PowerState/","" # If should be started and isn't, start VM 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-AzureRmVM } } # If should be stopped and isn't, stop VM 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-AzureRmVM -Force } } # Otherwise, current power state is correct else { Write-Output "[$($VirtualMachine.Name)]: Current power state [$currentStatus] is correct." } } # Main runbook content try { $currentTime = (Get-Date).ToUniversalTime() Write-Output "Runbook started. Version: $VERSION" if($Simulate) { Write-Output "*** Running in SIMULATE mode. No power actions will be taken. ***" } else { Write-Output "*** Running in LIVE mode. Schedules will be enforced. ***" } Write-Output "Current UTC/GMT time [$($currentTime.ToString("dddd, yyyy MMM dd HH:mm:ss"))] will be checked against schedules" # Retrieve subscription name from variable asset if not specified if($AzureSubscriptionName -eq "Use *Default Azure Subscription* Variable Value") { $AzureSubscriptionName = Get-AutomationVariable -Name "Default Azure Subscription" if($AzureSubscriptionName.length -gt 0) { Write-Output "Specified subscription name/ID: [$AzureSubscriptionName]" } else { throw "No subscription name was specified, and no variable asset with name 'Default Azure Subscription' was found. Either specify an Azure subscription name or define the default using a variable setting" } } # Retrieve credential write-output "Specified credential asset name: [$AzureCredentialName]" if($AzureCredentialName -eq "Use *Default Automation Credential* asset") { # By default, look for "Default Automation Credential" asset $azureCredential = Get-AutomationPSCredential -Name "Default Automation Credential" if($azureCredential -ne $null) { Write-Output "Attempting to authenticate as: [$($azureCredential.UserName)]" } else { throw "No automation credential name was specified, and no credential asset with name 'Default Automation Credential' was found. Either specify a stored credential name or define the default using a credential asset" } } else { # A different credential name was specified, attempt to load it $azureCredential = Get-AutomationPSCredential -Name $AzureCredentialName if($azureCredential -eq $null) { throw "Failed to get credential with name [$AzureCredentialName]" } } # Connect to Azure using credential asset (classic API) $account = Add-AzureAccount -Credential $azureCredential # Check for returned userID, indicating successful authentication if(Get-AzureAccount -Name $azureCredential.UserName) { Write-Output "Successfully authenticated as user: [$($azureCredential.UserName)]" } else { throw "Authentication failed for credential [$($azureCredential.UserName)]. Ensure a valid Azure Active Directory user account is specified which is configured as a co-administrator (using classic portal) and subscription owner (modern portal) on the target subscription. Verify you can log into the Azure portal using these credentials." } # Validate subscription $subscriptions = @(Get-AzureSubscription | where {$_.SubscriptionName -eq $AzureSubscriptionName -or $_.SubscriptionId -eq $AzureSubscriptionName}) if($subscriptions.Count -eq 1) { # Set working subscription $targetSubscription = $subscriptions | select -First 1 $targetSubscription | Select-AzureSubscription # Connect via Azure Resource Manager $resourceManagerContext = Add-AzureRmAccount -Credential $azureCredential -SubscriptionId $targetSubscription.SubscriptionId $currentSubscription = Get-AzureSubscription -Current Write-Output "Working against subscription: $($currentSubscription.SubscriptionName) ($($currentSubscription.SubscriptionId))" } else { if($subscription.Count -eq 0) { throw "No accessible subscription found with name or ID [$AzureSubscriptionName]. Check the runbook parameters and ensure user is a co-administrator on the target subscription." } elseif($subscriptions.Count -gt 1) { throw "More than one accessible subscription found with name or ID [$AzureSubscriptionName]. Please ensure your subscription names are unique, or specify the ID instead" } } # Get a list of all virtual machines in subscription $resourceManagerVMList = @(Get-AzureRmResource | where {$_.ResourceType -like "Microsoft.*/virtualMachines"} | sort Name) $classicVMList = Get-AzureVM # Get resource groups that are tagged for automatic shutdown of resources $taggedResourceGroups = @(Get-AzureRmResourceGroup | where {$_.Tags.Count -gt 0 -and $_.Tags.Name -contains "AutoShutdownSchedule"}) $taggedResourceGroupNames = @($taggedResourceGroups | select -ExpandProperty ResourceGroupName) Write-Output "Found [$($taggedResourceGroups.Count)] schedule-tagged resource groups in subscription" # For each VM, determine # - Is it directly tagged for shutdown or member of a tagged resource group # - Is the current time within the tagged schedule # Then assert its correct power state based on the assigned schedule (if present) Write-Output "Processing [$($resourceManagerVMList.Count)] virtual machines found in subscription" foreach($vm in $resourceManagerVMList) { $schedule = $null # Check for direct tag or group-inherited tag if($vm.ResourceType -eq "Microsoft.Compute/virtualMachines" -and $vm.Tags -and $vm.Tags.Name -contains "AutoShutdownSchedule") { # VM has direct tag (possible for resource manager deployment model VMs). Prefer this tag schedule. $schedule = ($vm.Tags | where Name -eq "AutoShutdownSchedule")["Value"] Write-Output "[$($vm.Name)]: Found direct VM schedule tag with value: $schedule" } elseif($taggedResourceGroupNames -contains $vm.ResourceGroupName) { # VM belongs to a tagged resource group. Use the group tag $parentGroup = $taggedResourceGroups | where ResourceGroupName -eq $vm.ResourceGroupName $schedule = ($parentGroup.Tags | where Name -eq "AutoShutdownSchedule")["Value"] Write-Output "[$($vm.Name)]: Found parent resource group schedule tag with value: $schedule" } else { # No direct or inherited tag. Skip this VM. Write-Output "[$($vm.Name)]: Not tagged for shutdown directly or via membership in a tagged resource group. Skipping this VM." continue } # Check that tag value was succesfully obtained if($schedule -eq $null) { Write-Output "[$($vm.Name)]: Failed to get tagged schedule for virtual machine. Skipping this VM." continue } # Parse the ranges in the Tag value. Expects a string of comma-separated time ranges, or a single time range $timeRangeList = @($schedule -split "," | foreach {$_.Trim()}) # Check each range against the current time to see if any schedule is matched $scheduleMatched = $false $matchedSchedule = $null foreach($entry in $timeRangeList) { if((CheckScheduleEntry -TimeRange $entry) -eq $true) { $scheduleMatched = $true $matchedSchedule = $entry break } } # Enforce desired state for group resources based on result. if($scheduleMatched) { # Schedule is matched. Shut down the VM if it is running. Write-Output "[$($vm.Name)]: Current time [$currentTime] falls within the scheduled shutdown range [$matchedSchedule]" AssertVirtualMachinePowerState -VirtualMachine $vm -DesiredState "StoppedDeallocated" -ResourceManagerVMList $resourceManagerVMList -ClassicVMList $classicVMList -Simulate $Simulate } else { # Schedule not matched. Start VM if stopped. Write-Output "[$($vm.Name)]: Current time falls outside of all scheduled shutdown ranges." AssertVirtualMachinePowerState -VirtualMachine $vm -DesiredState "Started" -ResourceManagerVMList $resourceManagerVMList -ClassicVMList $classicVMList -Simulate $Simulate } } Write-Output "Finished processing virtual machine schedules" } catch { $errorMessage = $_.Exception.Message throw "Unexpected exception: $errorMessage" } finally { Write-Output "Runbook finished (Duration: $(("{0:hh\:mm\:ss}" -f ((Get-Date).ToUniversalTime() - $currentTime))))" }