Today it’s all about the money… oh well, about saving money.
Out of the box, Azure gives us the possibility to shutdown VMs at certain times. There is even option to switch them on at certain times, using Automation Accounts. But either per ResourceGroup or for the complete subscription. I needed more dynamic in terms of a “self-service”. How does it work?
A contributor which is responsible for a VM can assign Tags to a VM. Those define the status:
- Always ON
- Always OFF
- Start and stop automatically at certain times and days.
The options are:
- Any day (Mon,Tue,Wed,Thu,Fri,Sat,Sun)
- Start time (24h-format)
- Stop time (24h-format)
- UTC Offset of the time zone (with plus or minus or 0)
What we need:
- An Azure Automation Account with Run As Account
- The script from this post
- 10 minutes
- Colleagues that update their tags…
Let’s go!
1. Create an Azure Automation Account incl. Run As Account
No automation account, no automation. Of course, you can use an already existing account.
2. Making sure the modules are available
Since we need those modules within the script, please make sure they are available.
3. Update everything
In this case I used a new account not being used by anything else. Therefor I was able to simply update everything. If you are using an existing account, be careful! Make sure not make runbooks using legacy modules incompatible.
4. Delete the example runbooks
I don’t like junk.
5. Create new PowerShell runbook
6. Paste the script, save and publish
# Retrieve constants from Automation Account's variables $connectionName = Get-AutomationVariable connectionName $DefaultStartTime = Get-AutomationVariable DefaultStartTime $DefaultStopTime = Get-AutomationVariable DefaultStopTime $DefaultUtcOffset = Get-AutomationVariable DefaultUtcOffset $DefaultOnlineDays = Get-AutomationVariable DefaultOnlineDays function OnlineToday ($OnlineDays){ if ($OnlineDays){ $TodayValue = $CurrentDateUTC0.DayOfWeek.value__ $OnlineDays = $OnlineDays.Replace("Mon","1") $OnlineDays = $OnlineDays.Replace("Tue","2") $OnlineDays = $OnlineDays.Replace("Wed","3") $OnlineDays = $OnlineDays.Replace("Thu","4") $OnlineDays = $OnlineDays.Replace("Fri","5") $OnlineDays = $OnlineDays.Replace("Sat","6") $OnlineDays = $OnlineDays.Replace("Sun","7") if ($OnlineDays -like "*$TodayValue*"){ return $true } else{ return $false } } else{ return $true } } function OnlineNow ($OnlineStartUTC0, $OnlineStopUTC0){ if (($CurrentDateUTC0 -gt $OnlineStartUTC0) -and ($CurrentDateUTC0 -lt $OnlineStopUTC0)){ # VM should be online return $true } else{ # VM shoould be offline return $false } } function StartAzureVM ($VM){ if (!($VM.PowerState -eq "VM running")){ Write-Output "VM is starting." $VM | Start-AzureRmVM -AsJob } else{ Write-Output "VM is already running." } } function StopAzureVM ($VM){ if (!($VM.PowerState -eq "VM deallocated")){ Write-Output "VM shutting down." $VM | Stop-AzureRmVM -AsJob -Force } else{ Write-Output "VM is already deallocated." } } function TriggerAutoStartStop ($VM){ if(!$VM.Tags.StartTime){ $StartTime = $DefaultStartTime } else{ $StartTime = $VM.Tags.StartTime } if(!$VM.Tags.StopTime){ $StopTime = $DefaultStopTime } else{ $StopTime = $VM.Tags.StopTime } if(!$VM.Tags.UtcOffset){ $UtcOffset = $DefaultUtcOffset } else{ $UtcOffset = $VM.Tags.UtcOffset } if(!$VM.Tags.OnlineDays){ $OnlineDays = $DefaultOnlineDays } else{ $OnlineDays = $VM.Tags.OnlineDays } $OnlineStartUTC0 = (Get-Date $StartTime).AddHours(-$UtcOffset) $OnlineStopUTC0 = (Get-Date $StopTime).AddHours(-$UtcOffset) $CurrentDateUTC0 = (Get-Date).AddHours(-(Get-TimeZone).BaseUtcOffset.Hours) Write-Output "VM: $($VM.Name)" Write-Output "Auto online/offline desired." Write-Output "OnlineDays: $OnlineDays" Write-Output "OnlineStartUTC0: $OnlineStartUTC0" Write-Output "OnlineStopUTC0: $OnlineStopUTC0" Write-Output "CurrentDateUTC0: $CurrentDateUTC0" if (OnlineToday -OnlineDays $OnlineDays){ Write-Output "Not generally offline today. Now checking if it is in an offline or online timeframe." if (OnlineNow -OnlineStartUTC0 $OnlineStartUTC0 -OnlineStopUTC0 $OnlineStopUTC0){ Write-Output "VM needed right now." Write-Output "Starting VM if not already running." StartAzureVM -VM $VM } else{ Write-Output "VM not needed. Shut it down." Write-Output "Shutting VM down if not already shut down." StopAzureVM -VM $VM } } else{ Write-Output "Shut down VM. No need to have it for the whole." Write-Output "Shutting VM down if not already shut down." StopAzureVM -VM $VM } } function ProcessVM ($VM){ $AutoStartStop = $VM.Tags.AutoStartStop if($VM.Tags.AutoStartStop -eq "Auto"){ TriggerAutoStartStop -VM $VM } elseif($VM.Tags.AutoStartStop -eq "Online"){ Write-Output "VM should ALWAYS be running." Write-Output "Maybe check for reserved instances...?" } elseif($VM.Tags.AutoStartStop -eq "Offline"){ Write-Output "VM should be OFFLINE." Write-Output "Maybe check for reserved instances...?" StopAzureVM -VM $VM }else{ Write-Output "- Nothing -" # Maybe enforce it one day... # --> TriggerAutoStartStop with default values # Simply change the first elseif to if # and the original if to this else. } } function Login { try { # Get the connection "AzureRunAsConnection " $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName "Logging in to Azure..." Add-AzureRmAccount ` -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId ` -ApplicationId $servicePrincipalConnection.ApplicationId ` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint } catch { if (!$servicePrincipalConnection) { $ErrorMessage = "Connection $connectionName not found." throw $ErrorMessage } else{ Write-Error -Message $_.Exception throw $_.Exception } } } function StartCycle{ # All VMs Login $AllVMs = Get-AzureRmVM -Status foreach ($VM in $AllVMs){ Write-Output "Processing $($VM.Name)" ProcessVM -VM $VM Write-Output "----------------------" Write-Output "" } } StartCycle exit
7. Copy connection name
We need it for the variables.
8. Filling variables externally
The script uses some constants, which I prefer pulling externally. Those are in the blade holding the variables. Either use the names exactly the way I have them or adjust the script accordingly.
Do this with all variables you see in this screenshot.
9. Create schedules and link them to the runbook
In order to automate it, we need triggers. I decided to have the script run every 15 minutes. Adjust to fit your needs.
Do this with all schedules.
10. Update the tags on your VMs
The most important tag is “AutoStartStop”! Following values are enumerated:
- Auto – VM will be shut down or switched on according the defined schema.
- Online – VM should always be online. Reserved instances might make sense here…
- Offline – VM should always be offline. Reserved instances don’t make sense here…
If the tag is not set, NOTHING happens! No worries 🙂
Now, if it is set to “Auto” following tags can be defined as well:
- StartTime – Starting at what time should the VM be running. CAUTION: Only 24h-format!
- StopTime – Starting at what time should the VM be offline. CAUTION: Only 24h-format!
- UtcOffset – Which Utc time zone are you defining these times for? We’re making sure not to get confused with times within a global setup.
- OnlineDays – On which days should the VM be running at all. If a day is not in here, the VM will be off for this complete day, period!
If no values are configured, the default values from the variables of the Automation Account are being used.
11. Test
Now you should make sure everything is working. Do so by editing and testing the runbook.
When changing the values of the tags, we will receive different result.
Final words
The script has the disadvantage of not being capable of running across a day. It also can’t have multiple triggers on the same day. So, Friday 20:00 until Saturday 04:00 isn’t possible (, only if you use the UtcOffset as a “hack”). Or Mon-Fri 08:00 – 17:00, but on Sa-So 08:00 – 12:00.
But honestly… First of all, I would like to see which of our departments ist going to use this in the first place. And saving 50 Euros a couple of hundred times a week, instead of 80 Euros, is still better than saving nothing at all.