Shutting down VMs on schedule in Azure
Solution ·I thought it was time for a post on the quintessential automation task: shutting down VMs & starting them on a schedule.
UPDATE (Nov-27th 2016): this particular task is now available directly in the VM portal (at least the shut down part of it). This article remains interesting to look at a complete example of Azure Automation.
This is a perfect job for Azure Automation / Runbook and will allow us to discover plenty of details.
I took some inspiration from Noah Stahl on automys.com. His approach was quite original as he uses the Azure Tags to input a schedule for a VM to be up / down.
I kept the tag idea but left the scheduling to Azure Automation as it does it much better and more robustly.
Functional
So the way this is gona work is that we’re going to have a runbook running a PowerShell workflow. This script will scan your entire subscription for VMs with a specified tag on them. For those, it will shut them down.
A similar runbook will start-up those VMs.
Azure Automation
Please see Azure Runbook – A complete (simple) example in order to create an Automation account.
Credentials
Once created, we should create a credential with a Service Principal. Please read through Using Azure Active Directory Service Principal in order to create a Service Principal.
Click Add a credential.
And fill the fields the following way:
- Name: principal
- Description: something like “Principal used to start / shutdown VMs”
- User Name: the Client ID of the AAD Application (Service Principal)
- Password: the key of the AAD Application
Then we’re going to give some access to this principal so it can see VMs and start / shut them down.
The easiest way is to go in a Resource Group containing a VM and adding access.
Add a user, give it the Virtual Machine Contributor role and type the name of the AAD application for user name.
Shutdown Runbook
At the center of this runbook is the Stop-AzureRmVm cmdlet.
workflow Shutdown-VMs
{
Param
(
[parameter(Mandatory=$true)]
[String] $tagKey="env",
[parameter(Mandatory=$true)]
[String] $tagValue="dev"
)
# Refered to the credential stored in the Azure Automation account
$credentialAssetName = "principal"
$cred = Get-AutomationPSCredential -Name $credentialAssetName
if(!$cred)
{
Throw "Could not find an Automation Credential Asset named '${credentialAssetName}'. Make sure you have created one in this Automation Account."
}
# Connect to your Azure subscription
$account = Add-AzureRmAccount -ServicePrincipal -Credential $cred -Tenant "72f988bf-86f1-41af-91ab-2d7cd011db47"
if(!$account)
{
Throw "Could not authenticate to Azure using the credential asset '${credentialAssetName}'. Make sure the user name and password are correct."
}
# Get all the VMs in the subscription (the ones the principal is able to see)
# Filter on the ones having the tag specified in the runbook parameter
$vms = Get-AzureRmVM | Where-Object {$_.Tags[$tagKey] -eq $tagValue}
# Shutdown-VMs in parallel
ForEach -Parallel ($v in $vms)
{
Write-Output "Stopping $($v.ResourceGroupName).$($v.Name)"
$ops = Stop-AzureRmVM -ResourceGroupName $v.ResourceGroupName -Name $v.Name -Force
if($ops.IsSuccessStatusCode -ine $true)
{
Write-Output "Failure to stop $($v.ResourceGroupName).$($v.Name)"
}
else
{
Write-Output "$($v.ResourceGroupName).$($v.Name) Stopped"
}
}
}
You can save, test and publish the runbook.
Start Runbook
At the center of this other runbook is the Start-AzureRmVm cmdlet. It is basically identical to the other one except for this cmdlet.
workflow Start-VMs
{
Param
(
[parameter(Mandatory=$true)]
[String] $tagKey="env",
[parameter(Mandatory=$true)]
[String] $tagValue="dev"
)
# Refered to the credential stored in the Azure Automation account
$credentialAssetName = "principal"
$cred = Get-AutomationPSCredential -Name $credentialAssetName
if(!$cred)
{
Throw "Could not find an Automation Credential Asset named '${credentialAssetName}'. Make sure you have created one in this Automation Account."
}
# Connect to your Azure subscription
$account = Add-AzureRmAccount -ServicePrincipal -Credential $cred -Tenant "72f988bf-86f1-41af-91ab-2d7cd011db47"
if(!$account)
{
Throw "Could not authenticate to Azure using the credential asset '${credentialAssetName}'. Make sure the user name and password are correct."
}
# Get all the VMs in the subscription (the ones the principal is able to see)
# Filter on the ones having the tag specified in the runbook parameter
$vms = Get-AzureRmVM | Where-Object {$_.Tags[$tagKey] -eq $tagValue}
# Start-VMs in parallel
ForEach -Parallel ($v in $vms)
{
Write-Output "Starting $($v.ResourceGroupName).$($v.Name)"
$ops = Start-AzureRmVM -ResourceGroupName $v.ResourceGroupName -Name $v.Name
if($ops.IsSuccessStatusCode -ine $true)
{
Write-Output "Failure to start $($v.ResourceGroupName).$($v.Name)"
Write-Output $ops
}
else
{
Write-Output "$($v.ResourceGroupName).$($v.Name) Started"
}
}
}
Scheduling
Now that we have both runbooks we can give them a schedule by creating a job for each.
Conclusion
We’ve seen how to create 2 simple runbooks to start & shut down VMs with an Azure Service Principal.
You could create multiple jobs using different schedule and different tags. For instance, you might want your developer VMs to be off during a certain schedule but your QA to be off on a different schedule. By assigning different tags to different VMs, this would be easily accomplished.
UPDATE (05-02-2016): In some cases, your VM might fail to restart. Read Allocation Failure and Remediation to understand why. The solution to that would be to destroy the VMs (keep the VHDs) (instead of simply shutting them down) and recreating them. A colleague of mine wrote the post On & Off – Done Right on Azure to explain how to go about it. You could hook up those scripts in the automation built here and have a failure-resistant automation. Those script wouldn’t be generic though: they would depend on your VM configuration.
2 responses