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:

Microsoft Azure VM Power Scheduling, Simplified: SkyPlug

 

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.

 

Using tags to control power schedules
Using tags to control power schedules

 

Scheduled startup/shutdown runbook output example
Scheduled startup/shutdown runbook output example

 

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.

 

Use tags to define shutdown schedules
Use tags to define shutdown schedules

 

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:

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.

 

Testing schedule text. Somehow PowerShell doesn't know about Hammer Time.
Testing schedule text. Somehow PowerShell doesn't know about Hammer Time.

 

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:

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).

 

Using "Simulate" option to safely test
Using "Simulate" option to safely test

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:

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:

  1. Open subscription in https://portal.azure.com

  2. Open the Automation Account which will contain the runbook

  3. Open the Runbooks view from the Resources section

  4. Click Add a runbook from the top menu

  5. Select Import an existing runbook

  6. Click Create to upload

  7. Confirm “Assert-AutoShutdownSchedule” now appears in the runbooks list

  8. Open the runbook from the list

  9. Click Edit from the top menu

  10. Click Publish from the top menu and confirm

  11. Confirm the runbook now shows a status of Published

 

Import runbook from downloaded ps1 file
Import runbook from downloaded ps1 file

 

Automation automation runbook 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:

  1. 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.

  2. Open subscription in https://portal.azure.com

  3. Open the Automation Account which will contain the runbook

  4. Open the Assets view from the resources section

  5. Open the Credentials view

  6. Click Add a credential from the top menu

  7. Enter details for the new credential. Recommended to use name “Default Automation Credential”.

  8. Click Create

 

Create new Azure Automation credential asset

 

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.

  1. Open subscription in https://portal.azure.com

  2. Note your target subscription name as shown in Browse > Subscriptions

  3. Open the Automation Account which will contain the runbook

  4. Open the Assets view from the resources section

  5. Open the Variables view

  6. Click Add a variable from the top menu

  7. Give the variable a name (“Default Azure Subscription” expected by default), and enter the subscription name as the variable’s value. Click Create.

 

Create Azure Automation variable for subscription name

 

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:

  1. Back in the runbooks list, open the new runbook “Asset-AutoShutdownSchedule”

  2. Open the Schedules view under details

  3. Click Add a schedule in top menu

  4. Click Link a schedule to your runbook

  5. Click Create a new schedule

  6. Provide a name like “Hourly Runbook Schedule”

  7. Set the start time to time you want to first run, e.g. the next upcoming hour mark

  8. Set Recurrence to Hourly

  9. Click Create

  10. (Optional) If you want to provide a credential or subscription name directly and didn’t use the default names, click Configure your runbook parameters

  11. (Optional) Enter the name of the credential asset the runbook should use

  12. (Optional) Enter the name of the subscription the runbook should use

  13. Click OK to close the open dialogs

  14. Confirm the schedule now appears in the list with status Enabled

 

Creating a schedule for the runbook to check VM shutdown schedule tags

 

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:

  1. Open subscription in https://portal.azure.com

  2. Navigate to Browse > Resource Groups, and open a resource group that contains VMs to schedule

  3. Click the tag icon in the upper right

  4. In the Key field, enter “AutoShutdownSchedule

  5. In the Value field, enter a schedule as discussed above, such as “10PM -> 6AM”

  6. Click Save in the top menu

 

Azure resource group tag configuration
Azure resource group tag configuration

 

Example shutdown schedule tag
Example shutdown schedule tag

 

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:

  1. 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”.

  2. Start the test VM(s)

  3. In the runbook view under your automation account, click the Start button from the top menu.

  4. 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

  5. Open the Output view, and wait for the runbook to execute. It takes a minute or two to queue and run.

 

Start runbook for a manual test
Start runbook for a manual test

 

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:

  1. 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).

  2. 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.

Testing runbook in Simulate mode
Testing runbook in Simulate mode

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.

 

Check runbook job logs for errors or warnings
Check runbook job logs for errors or warnings
Streams view contains exception details not shown in output view
All Logs view contains exception details not shown in output view

 

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.

 

Azure Automation account pricing tiers

 

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

Discuss