Microsoft Azure: Virtuelle Maschinen zeitgesteuert automatisch starten und stoppen

Oftmals ist aus ausreichend, wenn virtuelle Maschinen bzw. Server in Microsoft Azure zeitgesteuert eingeschaltet sind. Gerade im Office-Umfeld reicht es beispielsweise, Server nur werktags zu den Arbeitszeiten zu aktivieren. Das kann auf Dauer richtig Geld sparen!

Manuelles Starten und Stoppen ist zwar jederzeit möglich, auf Dauer aber unbequem. Abhilfe schafft hier ein kleines Script, welches als Runbook in einem Automation-Konto nach einem festen Zeitplan läuft und die Aufgabe zuverlässig und automatisch durchführt.

Wie geht das?

Dieses Runbook automatisiert das planmäßige Starten und Herunterfahren von virtuellen Azure-Maschinen. Sie können mehrere individuelle Leistungspläne für Ihre virtuellen Maschinen mit einfachen Tag-Metadaten im Azure-Portal oder über PowerShell implementieren. Beispielsweise können Sie eine einzelne VM oder eine ganze Gruppe von VMs  von 10:00 Uhr und 6:00 Uhr, den ganzen Tag samstags und sonntags und an bestimmten Tagen des Jahres, wie z.B. an Feiertagen, heruntergefahren werden.

Das Runbook soll mittels eines Zeitplanes in einem Azure Automation-Konto mit einem konfigurierten Abonnement und zugehörigen Zugriffsberechtigungen ausgeführt werden. Beispielsweise kann es einmal pro Stunde ausgeführt werden und überprüft alle Zeitplan-Tags, die es auf Ihren virtuellen Maschinen oder Ressourcengruppen findet. Wenn die aktuelle Zeit innerhalb eines von Ihnen definierten Shutdown-Zeitraums fällt, beendet das Runbook die VM, wodurch keine Berechnungsgebühren entstehen. Wenn die aktuelle Zeit außerhalb eines markierten Shutdown-Zeitraums liegt, bedeutet dies, dass die VM ausgeführt werden soll, sodass das Runbook eine solche VM automatisch startet.

scheduled-virtual-machine-shutdown-startup-microsoft-azure-150531191605165

 

runbook_ergebnis

Ergebnis des Runbooks

Sobald das Runbook eingerichtet und geplant ist, ist es ausreichend, die Tags der einzelnen Ressourcen entsprechend anzupassen, damit das Runbook beim nächsten Lauf entsprechend reagiert – eine schnelle, einfache und übersichtliche Lösung für alle virtuellen Microsoft Azure Server!

 

Warum sollten Sie das Script nutzen?

Kosten sparen!

Der größte Kostenfaktur der Azure-Abo-Kosten bei der Verwendung von Virtual Machines (IaaS) ist die Laufzeit: wie viele Stunden laufen die VMs pro Monat. Wenn Sie VMs haben, die während bestimmter Zeiträume gestoppt werden können, können Sie die Rechnung reduzieren, indem Sie sie ausschalten (und die Bereitstellung aufheben).

Leider enthält Microsoft Azure direkt keine Werkzeuge, um einen Zeitplan wie diesen zu verwalten. Deswegen hilft dieses Script ungemein, und das ganze ohne Drittanbieter-Tools

Wie funktioniert es

Weitere Details: Zunächst müssen wir für jede Ressource den Zeitplan definieren. Zum Beispiel möchten wir vielleicht unsere VMs nach Geschäftsschluss abschalten und sie anlaufen lassen, bevor die Leute am Morgen im Büro ankommen. Oder wir wollen VM’s das ganze Wochenende über herunterfahren, nicht nur nachts. Oder an Feiertagen. Wir brauchen also einen flexiblen Ansatz für den Zeitplaner.

Man könnte natürlich ein fest definiertes Runbook zum Herunterfahren eines speziellen Servers nach einem eigenen Zeitplan verwenden. Bei mehreren Servern oder differenzierten Zeitplänen wird das aber schnell unübersichtlich. Deswegen macht es Sinn, von vornherein eine flexible, Tag-basierte Steuerung einzurichten und das Runbook und dessen Zeitplan allgemein zu halten. Wir verwenden einfach ein Tag auf eine virtuelle Maschine oder eine Azure-Ressourcengruppe, die VMs enthält. Dieses Tag ist eine einfache Zeichenfolge, die beschreibt, wie oft die VM heruntergefahren werden soll.

tag

Tag-Beschreibung

Das Runbook sucht nach einem Tag mit dem Namen „AutoShutdownSchedule“, der einer virtuellen Maschine oder Ressourcengruppe, die VMs enthält, zugewiesen wird. Der Wert dieses Tags sind ein oder mehrere Zeitplan-Einträge oder Zeitbereiche, die definieren, wann VMs heruntergefahren werden sollen. Anders herum gesagt sind die Zeiten, die hier nicht aufgeführt sind, die Zeiten, in denen die VM aktiv, also gestartet sein soll. Jedes Mal, wenn das Runbook die aktuelle Zeit gegen den Zeitplan prüft, stellt es sicher, dass die VM entsprechend ein- oder ausgeschaltet wird.

Wichtig: Das Script prüft gegen die Zeit aus der Zeitzone UTC/GMT, man muss also die Zeiten entsprechend „umrechnen“.

Es gibt zwei Arten von Einträgen:

  • Zeitbereich: Zwei Zeitangaben pro Tag oder absolutes Datum getrennt durch ‚->‘ (Strich größer-als). Verwenden Sie diese Option, um einen Zeitraum festzulegen, in dem die VMs heruntergefahren werden sollen.
  • Wochentag / Datum: Interpretiert als ein Tag, an dem VMs abgeschaltet werden sollen.

Alle Zeiten müssen Strings sein, die erfolgreich als „DateTime“ -Werte analysiert werden können. In anderen Worten, PowerShell muss in der Lage sein, den Text-Wert, den Sie zur Verfügung stellen, als Datums- und Zeitwert zu interpretieren. Es ist ein überraschendes Maß an Flexibilität erlaubt, und der einfachste Weg, einen vorgesehenen Wert zu testen, ist, eine PowerShell-Eingabeaufforderung zu öffnen und den Befehl

 Get-Date "<Zeit und/oder Datum>"

auszuführen, und sehen, was passiert. Wenn PowerShell einen formatierten Zeitstempel liefert, ist das gut. Wenn es beschwert, dass es nicht weiß, was gemeint ist, versuchen Sie, die Zeit anders zu schreiben.

get_date

Zeitplan-Tag-Beispiele

Der einfachste Weg, um den Zeitplan zu schreiben, ist, den Plan zuerst in Worte zu fassen. Danach kann man den Zeitplan dann Runbook-tauglich übersetzenDenken Sie daran, dass jeder Zeitraum, der nicht als Shutdown-Zeit definiert ist, zum Start der VM führt und damit kostenpflichtige Online-Zeit ist.

Sehen wir uns einige Beispiele an:

Herunterfahren von 10 Uhr bis 6 Uhr UTC jeden Tag

10pm -> 6am

Herunterfahren von 10 Uhr bis 6 Uhr UTC jeden Tag (anderes Format, dasselbe Ergebnis wie oben)

22:00 -> 06:00

Shut down von 8 Uhr bis 12 Uhr und von 2 Uhr bis 7 Uhr UTC jeden Tag (bringt online von 12-2am für die Wartung zwischen)

8 -> 12, 2 -> 7

Shut down den ganzen Tag Samstag und Sonntag (Mitternacht bis Mitternacht)

Samstag, Sonntag

Heruntergefahren von 2 Uhr bis 7 Uhr UTC jeden Tag und den ganzen Tag am Wochenende

2:00 -> 7:00, Samstag, Sonntag

Shut down zu Weihnachten und Neujahr

25. Dezember, 1. Januar

Heruntergefahren von 2 Uhr bis 7 Uhr UTC jeden Tag, und den ganzen Tag an Wochenenden und am Weihnachtstag

2:00 -> 7:00, Samstag, Sonntag, 25. Dezember

Shut down immer – ich will, dass diese VM nie automatisch läuft

0:00 -> 23:59:59

Was das Runbook macht

Das Runbook „AutoShutdownSchedule“ ist ein Azure Automation Runbook. Es kann einmal zu einem Zeitpunkt manuell ausgeführt werden, sollte aber so konfiguriert sein, dass es auf einem Zeitplan ausgeführt wird, z.B. einmal pro Stunde.

Das Runbook erwartet zwei Parameter: den Namen des Azure-Abonnements, das die VMs enthält, und den Namen eines Azure Automation-Berechtigungsnachweises mit gespeichertem Benutzernamen und Kennwort für das zu verwendende Konto für die Verbindung zu diesem Abonnement. Wenn nicht spezifisch konfiguriert, sucht das Runbook standardmäßig nach einem Credential-Asset mit dem Namen „Default Automation Credential“ und einer Variable mit dem Namen „Default Azure Subscription“. Diese Einstellungen werden im Folgenden näher erläutert. Es gibt einen dritten Parameter namens „Simulate“, der, wenn True, dem Runbook sagt, dass er nur Pläne auswerten, aber nicht umsetzen soll. Dies wird später beschrieben.

Nach der erfolgreichen Authentifizierung des Ziel-Abonnements sucht das Runbook nach einer beliebigen VM- oder Ressourcengruppe mit einem Tag „AutoShutdownSchedule“. Alle Ressourcengruppen ohne dieses spezifische Tag werden ignoriert. Für jede gefundene, mit Tag markierte Ressource, werden die Zeitpläne ausgewertet und gegen die aktuelle UTC-Systemzeit geprüft. Dabei werden folgende Ergebnisse vom Runbook ermittelt:

  • Wenn die aktuelle Zeit außerhalb der definierten Zeitpläne liegt, kommt das Runbook zu dem Schluss, dass dies die „Onlinezeit“ ist und startet jede direkt oder indirekt markierte VM, die gerade gestoppt ist.
  • Wenn die aktuelle Zeit zu einem der Zeitpläne passt, kommt das Runbook zu dem Schluss, dass dies die „Abschaltzeit“ ist und stoppt jede direkt oder indirekt markierte VM, die gerade eingeschaltet ist.
  • Wenn einer der definierten Zeitpläne nicht erkannt werden kann, wird er ignoriert und als Online-Zeit betrachtet. Das Verhalten im Fehlerfall bedeutet also, die betroffene VM zu starten oder aktiviert zu lassen; es werden keine VM’s gestoppt, wenn der Zeitplan einen Fehler enthält!

Runbook-Protokolle

Verschiedene Ausgabemeldungen werden vom Runbook jedes Mal aufgezeichnet, wenn es ausgeführt wird, und zeigt an, welche Aktionen durchgeführt wurden und ob Fehler bei der Verarbeitung von Tags oder beim Zugriff auf das Abonnement auftraten. Diese Protokolle können in der Ausgabe jedes Jobs gefunden werden.

Performance

Das Starten und Herunterfahren erfolgt nacheinander, jeweils eine VM nacheinander. Dies hilft, Probleme mit VMs zu vermeiden, die zu demselben Cloud-Dienst gehören, der nur eine Mitglieds-VM zu einem Zeitpunkt zum Ein- oder Ausschalten zulässt. Das Starten und Stoppen kann eine Minute oder zwei pro VM dauern, deswegen könnte es sich in großen Umgebungen lohnen, das Runbook anpassen, um parallele Aktionen auszuführen.

Testen

Um das Runbook zu testen, ohne Ihre VMs zu starten oder zu stoppen, können Sie die Option „Simulate“ verwenden. Wenn dieser Parameter auf „True“ gesetzt ist, werden die Zeitpläne ausgewertet, aber es werden keine Start- oder Stop-Maßnahmen ergriffen. Sie können dann sehen, ob alles erwartungsgemäß funktioniert, bevor Sie das Runbook live mittels Zeitplan aktivieren.

runbook_start

Einrichten in Azure

Jetzt gehen wir die Schritte durch, um dieses Runbook in Ihrem Azure-Abonnement zu aktivieren.

Voraussetzungen

Dies ist ein Azure Automation Runbook, und als solche müssen Sie Folgendes verwenden:

  • Microsoft Azure-Abonnement (einschließlich Testabonnements)
  • Azure Automation-Konto erstellt im Abonnement (Anweisungen)
  • Runbook-Datei

Importieren Sie das Runbook

Das Runbook ist als Download verfügbar. Sie können diese wie folgt in Ihr Automatisierungskonto importieren:

  1. Anmeldung unter https://portal.azure.com
  2. Öffnen Sie das Automatisierungskonto, das das Runbook enthält
  3. Öffnen Sie die Runbooks-Ansicht aus dem Abschnitt Ressourcen
  4. Klicken Sie im oberen Menü auf Runbook hinzufügen
  5. Wählen Sie Ein vorhandenes Runbook importieren
  6. Klicken Sie zum Hochladen auf Erstellen
  7. Prüfen Sie, dass „AutoShutdownSchedule“ nun in der Runbooks-Liste angezeigt wird
  8. Öffnen Sie das Runbook aus der Liste
  9. Klicken Sie im oberen Menü auf Bearbeiten
  10. Klicken Sie im oberen Menü auf Veröffentlichen und prüfen Sie, dass der Status „veröffentlicht“ ist.

 

runbook_import

 

runbook_veroeffentlicht

 

Credential-Asset erstellen

Wenn das Runbook ausgeführt wird, greift es auf Ihr Abonnement mit den von Ihnen konfigurierten Anmeldeinformationen zu. Standardmäßig sucht es nach einem Credential namens „Default Automation Credential“ und verzweigt dort auf einen Benutzer, den Sie im Azure Active Directory des Abonnements erstellen und die Berechtigungen zum Verwalten von Abonnementressourcen erteilen, z.B. als Co-Administrator.

Die Schritte:

  1. Erstellen Sie einen Azure Active Directory-Benutzer für die Verwendung im Runbook, wenn Sie dies noch nicht getan haben. Dieses Konto ist das „Dienstkonto“ für das Runbook und es muss ein Co-Administrator im Ziel-Abonnement sein
  2. Anmeldung unter https://portal.azure.com
  3. Öffnen Sie das Automatisierungskonto, das das Runbook enthält
  4. Öffnen Sie die Ressourcenansicht aus dem Abschnitt Ressourcen
  5. Öffnen Sie die Anmeldeinformationenansicht
  6. Klicken Sie im oberen Menü auf Anmeldeinformation hinzufügen
  7. Geben Sie die Details für die neue Berechtigung ein. Empfohlen wird, den Namen „Default Automation Credential“ zu verwenden.
  8. Klicken Sie auf Erstellen

credentials

Variable für Abonnementname erstellen

Das Runbook muss auch wissen, mit welchem Abonnement zu sich verbinden soll, wenn es ausgeführt wird.Theoretisch kann ein Runbook eine Verbindung zu einem beliebigen Abonnement herstellen, so dass wir es beim Ausführen mit angeben müssen. Dies geschieht einfach durch das Einrichten einer Variablen in unserem Automatisierungskonto.

  1. Anmeldung unter https://portal.azure.com
  2. Notieren Sie den Namen Ihres Zielabonnements, wie in Durchsuchen> Abonnements angezeigt
  3. Öffnen Sie das Automatisierungskonto, das das Runbook enthält
  4. Öffnen Sie die Ressourcenansicht aus dem Abschnitt Ressourcen
  5. Öffnen Sie die Ansicht Variablen
  6. Klicken Sie im oberen Menü auf Variable hinzufügen
  7. Geben Sie der Variablen einen Namen (standardmäßig „Default Azure Subscription“) und geben Sie den Namen der Variablen als Variablenwert ein.
  8. Klicken Sie auf Erstellen.

variable

Planen Sie das Runbook

Das Runbook sollte in regelmäßigen Abständen ausgeführt werden. Wie zuvor beschrieben bestimmt dies nicht den Ein- / Ausschaltzeitplan der einzelnen Server. Es bestimmt nur, wie oft die Zeitpläne der einzelnen Ressourcen gegen die aktuelle Systemzeit überprüft werden. Azure erlaubt bis zu einer stündlichen Frequenz:

  1. Öffnen Sie in der Runbooks-Liste das neue Runbook „AutoShutdownSchedule“
  2. Öffnen Sie die Zeitplanansicht unter Details
  3. Klicken Sie im oberen Menü auf Plan hinzufügen
  4. Klicken Sie auf Verknüpfen eines Zeitplans mit Ihrem Runbook
  5. Klicken Sie auf Neuen Zeitplan erstellen
  6. Geben Sie einen Namen wie „Stündlicher Zeitplan“
  7. Stellen Sie die Startzeit auf die Zeit ein, die Sie zuerst ausführen möchten, z.B. das nächste volle Stunde
  8. Set Wiederholung aufStündlich
  9. Klicken Sie auf Erstellen

Optional:

  • Wenn Sie einen Credential- oder Subskriptionsnamen direkt angeben und die Standardnamen nicht verwenden möchten, klicken Sie auf Configure your runbook parameters
  • Geben Sie den Namen des Berechtigung (Credentials) ein, den das Runbook verwenden soll
  • Geben Sie den Namen des Abonnements ein, das das Runbook verwenden soll
  • Klicken Sie auf OK, um die geöffneten Dialoge zu schließen
  • Bestätigen Sie, dass der Zeitplan nun in der Liste mit dem Status aktiviert erscheint

zeitplan

Konfigurieren von Shutdown Schedule Tags

Schließlich müssen wir unsere VM-Ressourcengruppen taggen. Das Tag-Format wurde oben beschrieben. So erstellen Sie Zeitplan-Tags:

  1. Anmeldung unter https://portal.azure.com
  2. Navigieren Sie zu Durchsuchen> Ressourcengruppen, und öffnen Sie eine Ressourcengruppe, die VMs zum Planen enthält
  3. Klicken Sie oben rechts auf das Tag-Symbol
  4. Geben Sie im Feld Schlüssel „AutoShutdownSchedule“
  5. Geben Sie im Feld Wert einen Zeitplan wie oben beschrieben ein, z. B. „01:30 -> 06:00“
  6. Klicken Sie im oberen Menü auf Speichern

tag

Nach Sie diesen Vorgangs für jede VM-Ressourcengruppe in Ihrem Abonnement wiederholt haben, ist alles vorbereitet, um die virtuellen Maschinen automatisch herunterzufahren und zu starten.Sie können die Tags jederzeit aktualisieren und änderen, ohne den Code des Runbooks anpassen zu müssen.Denken Sie daran, dass VMs in nicht getaggten Ressourcengruppen nicht vom Runbook verwaltet werden.

Erstprüfung

Um zu überprüfen, dass das Runbook funktioniert, können wir einen ersten Test manuell ausführen und die Ergebnisse überprüfen. Das ist einfach:

Weisen Sie der VM oder Ressourcengruppe, die Sie zum Testen verwenden möchten, ein Shutdown-Terminplan-Tag zu. Geben Sie ihm einen Zeitplan, der die aktuelle Zeit abdeckt. Der einfachste Weg ist, den heutigen Tag der Woche zu verwenden, z.B. „Mittwoch“.

  1. Starten der Test-VM
  2. Klicken Sie in der Runbook-Ansicht unter Ihrem Automatisierungskonto auf die Schaltfläche Start im oberen Menü.
  3. Überprüfen Sie, ob die Parameter korrekt sind, wenn Sie nicht die Standardwerte verwenden möchten. Setzen Sie den Paramenter „Simulate“ auf „True“, um ohne Änderungen zu testen. Wählen Sie „Ausführen“ in Azure aus, und klicken Sie auf OK
  4. Öffnen Sie die Ausgabe-Ansicht, und warten Sie, bis das Runbook ausgeführt wird. Es dauert ca. 1-2 Minuten.

Jetzt hoffen wir, dass Nachrichten in der Ausgabe angezeigt werden, die uns zeigen, dass eine markierte VM- oder Ressourcengruppe gefunden wurde, dass die aktuelle Zeit innerhalb eines Shutdown-Zeitplans liegt und dass die beabsichtigten VMs bei einer normalen Ausführung gestoppt worden wären. Eventuell auftretende Fehler sollten auch in der Ausgabe aufgezeichnet werden.

Testen Sie danach den umgekehrten Fall: Starten von VMs, die entsprechend dem Zeitplan ausgeführt werden sollen (wenn sie sich nicht in einem explizit definierten Abschaltzeitraum befinden, sollten sie gestartet werden). Hierfür können wir unser Zeitplan-Tag aktualisieren und erneut wie folgt testen:

  1. Kehren Sie zur Test-VM- oder Ressourcengruppe zurück und legen Sie das AutoShutdownSchedule so fest, dass es die aktuelle Zeit nicht abdeckt. Zum Beispiel, wenn heute Mittwoch ist, setzen Sie den Tag-Wert auf „Dienstag“. Durch erneutes Setzen des Tags wird das bestehende Tag mit demselben Namen überschrieben (Hinweis: Sie können das Dropdown-Menü verwenden, um vorherige Tag-Tasten und Werte auszuwählen).
  2. Starten Sie nun das Runbook mit den gleichen Schritten wie zuvor und beobachten Sie die Ausgabe

Dieses Mal sollten wir sehen, dass die aktuelle Uhrzeit nicht mit denAbschaltplänen für die VM oder Gruppe übereinstimmt, und Sie sehen im Runbook-Bericht, dass die gewünschten VMs gestartet werden würden.

Fehlerbehebung

Um nach Problemen zu suchen, können Sie den Jobprotokoll des Runbooks untersuchen und die Ausgabe und den Streams / Verlauf für jeden einzelnen Job zu betrachten. Im neuen Portal enthält die Ausgabe-Ansicht nicht unbedingt Fehlerdetails, daher sollten Sie auch die Stream-Ansicht überprüfen.

Konfiguration des Automations-Kontos

Bevor das Runbook in einer produktiven Umgebung aktiviert wird (was Sie sicherlich vorhaben) empfehle ich Ihnen, Ihr Automatisierungskonto als „Basic“ anstelle eines kostenlosen Kontos („Free) zu konfigurieren. Dadurch wird sichergestellt, dass die 500-Minuten Laufzeitbegrenzung pro Monat nicht greift und das Runbook sicher läuft. Die Kosten für die Ausführungszeit des Runbooks sind extrem niedrig für zusätzliche Minuten, so dass der geringe Aufpreis leicht durch die Berechnung der kostenpflichtigen VM-Laufzeiten ausgeglichen werden kann.

Der Code

<#
    .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:
        https://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))))"
}

 

Schreibe einen Kommentar