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