Zu einem bestehenden 2-Knoten S2D-Hyper-V-Failover-Cluster aus 2 exakt gleichen Hosts hatte ich vor kurzem einen dritten Knoten hinzugefügt. Es handelte sich hierbei um das selbe HP-Server-Modell. Bei den ersten Live-Migrationen traten jedoch Fehler auf und eine Analyse brachte zu Tage, dass die CPUs am dritten Knoten geringfügig andere sind.
Für dieses Problem gibt es im Allgemeinen das Feature „Processor compatibility“ in Hyper-V (Im Deutschen als „Prozessorkompatibilitätsmodus“ bezeichnet, in der GUI heisst es dann „Zu einem physischen Computer mit einer anderen Prozessorversion migrieren“ bzw. „Migrate to physical computer with a different processor version“). Dieses Feature kann man natürlich problemlos über die GUI ein- und ausschalten:
Allerdings ergeben sich hier schnell 2 Herausforderungen:
Das Feature lässt sich nur ändern, während die VM ausgeschaltet ist
In einem Cluster laufen schnell mal eine 2-stellige Anzahl VMs
Das kann man natürlich wie so vieles sehr elegant per PowerShell lösen, also habe ich dafür ein passendes Skript geschrieben. Dieses kann entweder zum ein- oder ausschalten des Features benutzt werden und iteriert dabei über einen Cluster und behandelt dabei dann alle VMs im Cluster, bei denen das Feature noch nicht korrekt eingestellt ist, in dem die VMs bei Bedarf zuerst gestoppt, das Feature gesetzt und dann falls vorher laufend die VM wieder startet. Am Ende wird dann noch der Gesamtzustand ausgegeben.
Das Skript findet ihr auf meinem GitHub Repo, siehe hier:
Damit kann man nun also entweder bei unterschiedlicher Hardware die CPU-Kompatibilität für alle VMs erhöhen oder, nachdem man z.B. die CPUs überall auf den selben Stand gebracht hat, das Feature wieder deaktivieren.
Azure Migrate bietet im Alltag viele sehr nützliche Funktionen. Dabei können unter anderem Assessments auf bestehende on-premises Umgebung gefahren werden, um neben einer Kostenindikation auch einen ersten Überblick über die technische Machbarkeit einer Lift-and-Shift-Migration von Servern von on-premises zu erhalten.
Insbesondere für diese Assessments (die dann u.a. auch eine Depency Analysis erlauben) werden Gruppen in Azure Migrate benötigt. Diese kann man im Portal nur sehr rudimentär anlegen. Dabei müssen aus der Liste von erkannten on-opremises Servern die gewünschten Systeme in mehreren Listen-Seiten zu je ca. 10 Systemen, die sich weder sortieren noch (sinnvoll) filtern lässt, ausgewählt und mit Checkboxen zur Gruppe hinzugefügt werden:
Nun gibt es zwar seit Ende 2020 ein PowerShell-Modul für Azure Migrate – dieses kann aber bis heute in keinster Weise mit diesen Gruppen umgehen – weder diese Abfragen, noch anlegen oder verändern.
Aus diesem Grund – und weil es immer öfter nötig ist, aus hunderten oder tausenden erkannten on-prem Systemen nur einen Teil auszuwählen – habe ich ein passendes PowerShell-Skript erstellt, welches eine über Hostnamen gegebene Liste von Servern in eine entweder bereits vorhandene oder im Laufe des Skriptes angelegte Gruppe aufnimmt. Dazu werden verschiedene REST-Calls gegen die REST-API von Azure benutzt, um sowohl mit den Gruppen als auch den erkannten Servern umgehen zu können.
Vor einiger Zeit (muss mehrere Jahre her sein) habe ich ein paar Azure Automation PowerShell Runbooks auf GitHub und hier veröffentlich, um VMs Zeit- und Tag- gesteuert starten und stoppen zu können und um heruntergefahren, nicht-deallokierte VMs zu deallokieren. Diese basierten auf AzureRM PowerShell Cmdlets / Modulen und einer etwas komplizierten Herangehensweise. Ich habe die Runbooks nun massiv überarbeitet, so dass diese erstens das neuere Az Modul verwenden und auch vom Aufbau her wesentlich einfacher sind. U.a. verwenden die Runbooks jetzt lokale Deutsche Zeit und sind robuster bei der Schreibweise der Tags (Groß-/Kleinschreibung).
Ihr findet diese Runbooks wie immer in meinem GitHub Repo, konkret genau hier:
Immer wieder ist es für Automatisierungs- oder Optimierungsthemen sinnvoll, Preise für Azure Services abzufragen – und zwar so, dass man sie automatisiert bzw. in Skripten verwerten kann.
Dazu gibt es eine recht simple Lösung – und natürlich hat sie mit PowerShell zu tun 🙂
Es gibt eine API, die man abfragen kann. Diese ist unter
https://prices.azure.com/api/retail/prices
zu erreichen. Sie liefert bei direkter Abfrage eine riesige Menge an Daten, so dass es sinnvoll ist, schon bei der Abfrage zu filtern. Außerdem muss man die Abfrage in mehreren Durchläufen verwenden, sonst kommt es ggf. zu Timeouts oder es kommenden nicht alle Daten zurück.
Das Code-Schnippselchen, dass das erledigt, sieht dann folgendermaßen aus:
$pricelistApiUrl="https://prices.azure.com/api/retail/prices?`$filter=armRegionName eq 'westeurope' and serviceName eq 'Virtual Machines'"$reponseSkus=@()do{$webRequest= Invoke-WebRequest -Uri $pricelistApiUrl| ConvertFrom-Json
$pricelistApiUrl=$webRequest.NextPageLink
$reponseSkus+=$webRequest.Items
#$pricelistApiUrl}while($pricelistApiUrl-ne$null)$vmPrices=$reponseSkus|Where-Object{($_.type-eq"Consumption")-and($_.skuName -notlike"*Low Priority*")-and($_.skuName -notlike"*Spot*")}|Sort-Object armSkuName,retailPrice -Descending$vMPrices|Export-Csv-Path"VMPrices.csv"-Encoding UTF8 -NoTypeInformation-ErrorAction SilentlyContinue # In case the CSV is still open; For German systems, use '-Delimiter ";"'<p></p>
$pricelistApiUrl = "https://prices.azure.com/api/retail/prices?`$filter=armRegionName eq 'westeurope' and serviceName eq 'Virtual Machines'"
$reponseSkus = @()
do
{
$webRequest = Invoke-WebRequest -Uri $pricelistApiUrl | ConvertFrom-Json
$pricelistApiUrl = $webRequest.NextPageLink
$reponseSkus += $webRequest.Items
#$pricelistApiUrl
}
while ($pricelistApiUrl -ne $null)
$vmPrices = $reponseSkus | Where-Object {($_.type -eq "Consumption") -and ($_.skuName -notlike "*Low Priority*") -and ($_.skuName -notlike "*Spot*") }| Sort-Object armSkuName,retailPrice -Descending
$vMPrices | Export-Csv -Path "VMPrices.csv" -Encoding UTF8 -NoTypeInformation -ErrorAction SilentlyContinue # In case the CSV is still open; For German systems, use '-Delimiter ";"'<p></p>
Das Ganze lässt sich natürlich auch auf beliebige andere Regionen und Services oder auch auf die anderen Attribute / Spalten filtern. Allerdings werden die Preise ausschließlich in Dollar geliefert! Mehr dazu siehe:
Es gibt noch weitere API für Preise, die teilweise auch andere Funktionen, z.B. auch Währungsumrechnung, haben. Diese sind aber erstens scheinbar nicht vollständig und zweitens auch komplizierter in der Handhabung.
This is kind of an experiment. Usually, I produce all my content in German language, but to see if englisch content is of higher use for the community, I will give it a try…
In the last days, I dealt a lot with GitHub Actions to find out, how it can be used to deploy Azure ARM templates to the cloud. My last „evolution“ step now – I was working on this because of the very valuable hint of a collegue – is able to deploy ARM templates to all 4 available scope levels:
Tenant level
Management Group level
Subscription level
Resource Group level
And it also could be used, even when you don’t use GitHub Actions! But let’s start from the beginning…
Immer wieder begegnet mir das Problem, dass ich z.B. für ARM Templates den „internen“ Namen für eine Azure Region (dann als „Location“ bezeichnet) brauche. Und immer wieder liefern Suchmaschinen-Anfragen nur die „Sprechenden Namen“. Daher hier für mich und den Rest der Welt als kleiner Cheat:
Geht es um den Aufbau automatisierter CI/CD Pipelines für Azure, so denken die meisten wohl eher an Azure DevOps. Aber auch mit GitHub lässt sich so etwas erreichen – und zwar völlig kostenlos. Das Werkzeug dazu heißt GitHub Actions. Zu GitHub Actions selbst will ich hier gar nicht so viel schreiben – es gibt bereits einige Blogartikel und co. dazu. Ich verweise aber gerne auf mein Video, welches ich dazu gemacht habe:
Nun kam von einem meiner geschätzten Kollegen zu Recht die Frage, wie man denn in dieser (ersten, im Video gezeigten) Variante mehrere ARM Templates bereitstellen kann. Und dazu möchte ich hier die passende Antwort liefern…
Weil es die Tage bei einem meiner Kunden wieder mal ein Thema war, möchte ich es hier nun mal im Detail beleuchten. Und zwar geht es um die Anforderung, Virtuelle Maschinen in Azure starten und stoppen zu können, ohne beispielsweise die VM löschen zu können oder ihre SKU zu verändern. Natürlich gibt es den „Virtual Machine Contributor“ als built-in Role, aber diese darf eben deutlich zu viel.
Wie geht man nun an das Thema heran? Zunächst muss man zwei Dinge in Erfahrung bringen:
Den Namen des Resource-Providers um den es geht
Die Operationen auf diesem Provider, die man erlauben oder verbieten möchte
Hinweis: Der Großteil des Artikels ist auch in meinem YouTube-Video zu diesem Thema enthalten:
Auf Grund vieler Nachfragen aus Community und Kunden habe ich mich dazu entschieden, dieses Thema etwas genauer zu beleuchten… Wenn man Azure in größerem Umfang nutzt, dann kommt schnell die Frage nach interner Kostenverrechnung („Innerbetriebliche Leistungsverrechnung“ / „Charge back“) oder zumindest der Möglichkeit, die Kosten den jeweiligen Verursachern zuzuordnen und transparent darzustellen. Ein wesentliches Hilfsmittel dafür ist das Tagging von Ressourcen. Klar, man könnte die Ressourcen auch nach Kostenstellen getrennt in unterschiedlichen Subscriptions unterbringen, das stößt in der Praxis aber schnell an Grenzen und auf Probleme.
Hat man nun seine Ressourcen mit den entsprechenden Tags versehen, z.B. nach Besitzer („Owner“), Kostenstelle („Cost center“) oder Abteilung („Department“), dann tauchen diese Tags leider noch nicht in der regulären Rechnung auf.
Nun kann man sich aus dem Azure Portal (oder auch aus dem EA / Enterprise Portal oder via REST API) eine Detail-Aufstellung der Kosten herunterladen. Dazu muss man zu „Cost Management + Billing“ / Billing-Scope auswählen / „Usage + Charges“ / Monat wählen / Download-Button klicken / Kurz warten / “ Usage Details Version 2″ auswählen. Diese „Usage Details“ enthalten dann auch die Tags. Allerdings: Selbst wenn man das CSV-File so umformatiert, dass Excel damit umgehen kann, ergibt sich noch ein Problem:
Die einzelnen Tags werden alle zusammen geworfen und in einer „Spalte“ vermischt. Damit ist das Suchen oder Filtern nach einzelnen Tags schon recht schwer – eine bestimmte Kombination aus Tags ist damit unmöglich zu filtern. Dazu müsste man die Tags jeweils in einzelnen Spalten ablegen. Und da kommt PowerShell und dessen Möglichkeit, mit dem JSON-Format umzugehen, ins Spiel! Ich habe eine Script geschrieben, was die Spalte in die einzelnen Tags aufteilt. Dabei werden auch gleich Zahlen und Daten in das lokale Format überführt.
# Script to expand tags from the usage details CSV provided by Azure / Microsoft# to filter usage by tags; also converts some numbers to local format# Download CSV file by hand first!# This is needed for the FileOpen Dialog
Add-Type-AssemblyName System.Windows.Forms
$FileBrowser=New-Object System.Windows.Forms.OpenFileDialog -Property@{
InitialDirectory =[Environment]::GetFolderPath('Desktop')Filter='CSV-Files (*.csv)|*.csv'}$null=$FileBrowser.ShowDialog()# Just to open the dialogIf($FileBrowser.FileName -eq""){Write-Verbose"No file selected - aborting."Break}$CSV=Import-Csv$FileBrowser.FileName -Delimiter","for($i=0; $i-lt$CSV.length; $i++){# Showing progressWrite-Progress-Activity"Expanding in Progress..."-Status"$([math]::truncate($i / $($CSV.length) * 100))% complete..."-PercentComplete $($i/ $($CSV.length)*100)# Converting dates and numbers to local format$CSV[$i].Date =[datetime]::ParseExact($CSV[$i].Date,'MM/dd/yyyy',$null).ToString("d")$CSV[$i].BillingPeriodStartDate =[datetime]::ParseExact($CSV[$i].BillingPeriodStartDate,'MM/dd/yyyy',$null).ToString("d")$CSV[$i].BillingPeriodEndDate =[datetime]::ParseExact($CSV[$i].BillingPeriodEndDate,'MM/dd/yyyy',$null).ToString("d")$CSV[$i].Quantity =[float]$CSV[$i].Quantity
$CSV[$i].EffectivePrice =[float]$CSV[$i].EffectivePrice
$CSV[$i].Cost =[float]$CSV[$i].Cost
$CSV[$i].UnitPrice =[float]$CSV[$i].UnitPrice
# Expand Tags$Tags="{ $($CSV[$i].Tags) }"| ConvertFrom-Json # We need to add some brackets here...if($Tags-ne$null){$Tags.PSObject.Properties |ForEach{$TagName="Tag-$($_.Name)"Add-Member-InputObject$CSV[$i]$TagName$_.Value
# Adding the heading - what a rhyme (; ...if($CSV[0].PSObject.Properties[$TagName]-eq$null){Add-Member-InputObject$CSV[0]$TagName$null-Force}}}}# Saving as Excel-readable CSV$CSV|Export-Csv"$([System.IO.Path]::GetDirectoryName($FileBrowser.FileName))\$([io.path]::GetFileNameWithoutExtension($FileBrowser.FileName))_expanded.csv"-NoTypeInformation-Delimiter";"
# Script to expand tags from the usage details CSV provided by Azure / Microsoft
# to filter usage by tags; also converts some numbers to local format
# Download CSV file by hand first!
# This is needed for the FileOpen Dialog
Add-Type -AssemblyName System.Windows.Forms
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
InitialDirectory = [Environment]::GetFolderPath('Desktop')
Filter = 'CSV-Files (*.csv)|*.csv'
}
$null = $FileBrowser.ShowDialog() # Just to open the dialog
If($FileBrowser.FileName -eq "")
{
Write-Verbose "No file selected - aborting."
Break
}
$CSV = Import-Csv $FileBrowser.FileName -Delimiter ","
for ($i=0; $i -lt $CSV.length; $i++)
{
# Showing progress
Write-Progress -Activity "Expanding in Progress..." -Status "$([math]::truncate($i / $($CSV.length) * 100))% complete..." -PercentComplete $($i / $($CSV.length) * 100)
# Converting dates and numbers to local format
$CSV[$i].Date = [datetime]::ParseExact( $CSV[$i].Date, 'MM/dd/yyyy', $null).ToString("d")
$CSV[$i].BillingPeriodStartDate = [datetime]::ParseExact( $CSV[$i].BillingPeriodStartDate, 'MM/dd/yyyy', $null).ToString("d")
$CSV[$i].BillingPeriodEndDate = [datetime]::ParseExact( $CSV[$i].BillingPeriodEndDate, 'MM/dd/yyyy', $null).ToString("d")
$CSV[$i].Quantity = [float]$CSV[$i].Quantity
$CSV[$i].EffectivePrice = [float]$CSV[$i].EffectivePrice
$CSV[$i].Cost = [float]$CSV[$i].Cost
$CSV[$i].UnitPrice = [float]$CSV[$i].UnitPrice
# Expand Tags
$Tags = "{ $($CSV[$i].Tags) }" | ConvertFrom-Json # We need to add some brackets here...
if ($Tags -ne $null) {
$Tags.PSObject.Properties | ForEach {
$TagName = "Tag-$($_.Name)"
Add-Member -InputObject $CSV[$i] $TagName $_.Value
# Adding the heading - what a rhyme (; ...
if ($CSV[0].PSObject.Properties[$TagName] -eq $null) {
Add-Member -InputObject $CSV[0] $TagName $null -Force
}
}
}
}
# Saving as Excel-readable CSV
$CSV | Export-Csv "$([System.IO.Path]::GetDirectoryName($FileBrowser.FileName))\$([io.path]::GetFileNameWithoutExtension($FileBrowser.FileName))_expanded.csv" -NoTypeInformation -Delimiter ";"
Das Script findet ihr natürlich auch auf meinem GitHub Repo:
Das Resultat der Umwandlung wird dann in eine neue Excel-Datei im selben Folder wie die Originaldatei abgespeichert und kann dann problemlos in Excel geöffnet werden:
Wie man hier sieht sind die Tags nun in separaten Spalten untergebracht, so dass man sehr gut danach sortieren, darauf filtern oder mehr tun kann.
Ein regelmäßiges Problem im Azure-Alltag ist das Aufräumen nicht mehr benötigter Azure-Ressourcen. Diese kosten in der Regel unnötig Geld und stellen zum Teil auch ein (Sicherheits-)Risiko dar (wenn zum Beispiel eine nicht mehr verwendete VM aus dem Fokus gerät und über Jahre nicht gepatcht wird).
Um dieser Herausforderung zu begegnen gilt es zunächst, die Ressourcen, die „weg“ können, zu identifizieren. Dazu habe ich eine erste Version eines PowerShell Skriptes erstellt, welches:
Nicht verwendete Public IPs
Nicht verwendete NICs
Nicht verwendete NSGs
Nicht verwendete Managed Disks
findet und aufführt.
Über die Zeit möchte ich das Skript weiter ausbauen.
Um dir ein optimales Erlebnis zu bieten, verwenden wir Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wenn du diesen Technologien zustimmst, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn du deine Zustimmung nicht erteilst oder zurückziehst, können bestimmte Merkmale und Funktionen beeinträchtigt werden.
Funktional
Immer aktiv
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Vorlieben
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Statistiken
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt.Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Marketing
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.