Zum Inhalt

Asset-Abfragen

Abfragen für Azure Resource Graph und Log Analytics, um über einen verwalteten Bestand an virtuellen Maschinen zu berichten. Sie sind für die Azure-Monitor-Datenquelle in Grafana geschrieben: Die Resource-Graph-Abfragen laufen im Modus Azure Resource Graph, die letzte läuft im Modus Logs gegen den Log-Analytics-Workspace. Du kannst sie auch im Portal ausführen (Resource Graph Explorer, Log Analytics), wenn du die Zeile entfernst, die die Dashboard-Variable ${vms} verwendet.

Die Strategie, die diese Abfragen unterstützen, steht unter Asset-Management, das Modul unter Managed-VM-Modul.

Gilt für das Managed-VM-Modul

Die Abfragen nutzen die Tags, die das Managed-VM-Modul auf die Resource Group einer Konfigurationsmenge schreibt. Wenn deine Plattform kein solches Modul hat, nutze sie als Muster und ersetze den Tag-Filter durch die Tags deines eigenen Tag-Sets (Namen und Tags). Teste sie zuerst auf einem kleinen Scope.

So erkennst du eine Managed VM

Davon hängt ab, ob man einem Bericht trauen kann, deshalb sei es ausdrücklich gesagt. Das Modul wendet seine Plattform-Tags auf die Resource Group einer VM-Konfigurationsmenge an, ohne Bedingung: source, vm_configuration, environment, os_type, vm_size, public_ip, shutdown_schedule_hours und ein Tag ownerN pro Konfigurations-Owner. Die Tags, die ein Kunde definiert, landen stattdessen auf den VM-Ressourcen und sind optional.

Die verlässliche Definition lautet also:

Eine Managed VM ist eine virtuelle Maschine, deren Resource Group die Tags source und vm_configuration trägt, die das Modul gesetzt hat.

Jede Abfrage unten beginnt mit diesem Filter und verknüpft dann mit den Ressourcen, deshalb enthalten sie einen resourcecontainers- Join. Die Alternativen sind schlechter:

  • Filtern nach dem VM-Namen. Das Modul benennt Maschinen vm-<workload>-<configuration>-<env>-<location>-NN, aber nichts hindert ein Team daran, seine eigenen Maschinen genauso zu benennen, und die Legacy-Namensvariante (useOldVMNaming) folgt nicht dem Muster.
  • Filtern nach Tags der VM selbst. Das sind die Tags des Kunden, und sie können fehlen.
  • Filtern nach der Maintenance Configuration. Sie lässt sich pro Konfigurationsmenge mit disable_default_maintenance_configuration abschalten, sodass Maschinen fehlen würden.
  • Sich nur auf die Dashboard-Variable ${vms} verlassen. Sie ist ein Anzeigefilter, kein Scope-Filter. Wenn die Variable aus einer breiten Abfrage befüllt wird oder auf All steht, erscheinen nicht verwaltete Ressourcen im Ergebnis.

Die Variable ${vms}

Befülle die Dashboard-Variable aus derselben Definition, damit die Variable selbst immer nur Managed VMs anbietet:

resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id)
| join kind=inner (
    resources
    | where type =~ 'microsoft.compute/virtualmachines'
    | extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
    | project rgId, name
) on rgId
| distinct name
| order by name asc

In den Abfragen unten schränkt die Zeile mit ${vms} (zum Beispiel | where name in (${vms})) das Ergebnis auf die Auswahl im Dashboard ein. Entferne diese Zeile, um über jede Managed VM zu berichten, unabhängig von der Auswahl.

Resource Graph unterstützt nur eine Teilmenge von KQL. let und bag_keys() sind nicht verfügbar, und eine Abfrage darf höchstens drei join- und drei mv-expand-Operatoren enthalten. Die Abfragen unten bleiben innerhalb dieser Grenzen.

1. Inventar der Managed VMs

Die aktiven Managed VMs mit Konfiguration, Power State und Plattform-Tags.

resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id),
    vmConfiguration = tostring(tags['vm_configuration']),
    environment = tostring(tags['environment']),
    source = tostring(tags['source']),
    owner1 = tostring(tags['owner1']),
    owner2 = tostring(tags['owner2'])
| join kind=inner (
    resources
    | where type =~ 'microsoft.compute/virtualmachines'
    | extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
    | extend
        powerState = tostring(properties.extended.instanceView.powerState.code),
        vmSize = tostring(properties.hardwareProfile.vmSize),
        osType = tostring(properties.storageProfile.osDisk.osType),
        imageOffer = tostring(properties.storageProfile.imageReference.offer),
        imageSku = tostring(properties.storageProfile.imageReference.sku)
    | project rgId, name, powerState, vmSize, osType, imageOffer, imageSku,
        resourceGroup, location, subscriptionId
) on rgId
| where name in (${vms})
| project vmname = name, powerState, vmSize, osType, imageOffer, imageSku,
    vmConfiguration, environment, source, owner1, owner2,
    resourceGroup, location, subscriptionId
| order by vmname asc

2. Alle Assets der verwalteten Konfigurationsmengen

Jede Ressource, die zu einer verwalteten Konfigurationsmenge gehört, nicht nur die Maschinen: Disks, Netzwerkschnittstellen, Key Vault, Vaults, Data Collection Rules.

resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id),
    vmConfiguration = tostring(tags['vm_configuration']),
    environment = tostring(tags['environment'])
| join kind=inner (
    resources
    | extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
    | project rgId, name, type, location, resourceGroup, subscriptionId
) on rgId
| project vmConfiguration, environment, resourceType = type, name,
    resourceGroup, location, subscriptionId
| order by vmConfiguration asc, resourceType asc, name asc

Ergänze | summarize assetCount = count() by vmConfiguration, environment, resourceType für die Anzahl pro Typ statt der einzelnen Ressourcen.

3. Managed VMs pro Owner

Das Modul schreibt pro Konfigurations-Owner ein Tag ownerN. bag_keys() ist in Resource Graph nicht verfügbar, deshalb werden die Owner-Tags in ein Array gepackt und expandiert.

resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| extend owners = pack_array(
    tostring(tags['owner1']), tostring(tags['owner2']),
    tostring(tags['owner3']), tostring(tags['owner4']))
| mv-expand owner = owners to typeof(string)
| where isnotempty(owner)
| project rgId = tolower(id), owner, vmConfiguration = tostring(tags['vm_configuration'])
| join kind=inner (
    resources
    | where type =~ 'microsoft.compute/virtualmachines'
    | extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
    | project rgId, name, location, subscriptionId
) on rgId
| project owner, vmname = name, vmConfiguration, location, subscriptionId
| order by owner asc, vmname asc

Erweitere das pack_array, falls eine Konfigurationsmenge jemals mehr als vier Owners hat.

4. Tag-Abdeckung

Resource Groups, die virtuelle Maschinen enthalten, denen aber ein oder mehrere Plattform-Tags fehlen. Eine Konfigurationsmenge, die über das Modul deployt wurde, trägt sie immer, daher ist jedes Ergebnis hier eine Resource Group, die außerhalb der pipeline erstellt wurde.

resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| summarize vmCount = count() by rgId
| join kind=inner (
    resourcecontainers
    | where type =~ 'microsoft.resources/subscriptions/resourcegroups'
    | project rgId = tolower(id), resourceGroup = name, subscriptionId, tags
) on rgId
| extend
    hasSource = isnotempty(tostring(tags['source'])),
    hasVmConfiguration = isnotempty(tostring(tags['vm_configuration'])),
    hasEnvironment = isnotempty(tostring(tags['environment'])),
    hasOsType = isnotempty(tostring(tags['os_type'])),
    hasVmSize = isnotempty(tostring(tags['vm_size'])),
    hasOwner = isnotempty(tostring(tags['owner1']))
| where not(hasSource and hasVmConfiguration and hasEnvironment and hasOsType and hasVmSize and hasOwner)
| project resourceGroup, subscriptionId, vmCount,
    hasSource, hasVmConfiguration, hasEnvironment, hasOsType, hasVmSize, hasOwner
| order by resourceGroup asc

5. Disks der Managed VMs

Speicher als Teil des Asset-Eintrags: Größe, SKU und Verschlüsselung.

resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
| join kind=inner (
    resources
    | where type =~ 'microsoft.compute/disks'
    | extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
    | extend
        diskSizeGb = toint(properties.diskSizeGB),
        diskState = tostring(properties.diskState),
        skuName = tostring(sku.name),
        encryptionType = tostring(properties.encryption.type),
        attachedTo = tostring(split(tostring(properties.managedBy), '/', 8)[0])
    | project rgId, diskName = name, diskSizeGb, skuName, diskState,
        encryptionType, attachedTo, resourceGroup
) on rgId
| where isnotempty(attachedTo)
| where attachedTo in (${vms})
| project vmname = attachedTo, diskName, diskSizeGb, skuName, diskState,
    encryptionType, vmConfiguration, resourceGroup
| order by vmname asc, diskName asc

6. Backup-Schutzstatus

Beide Vaults werden in die Resource Group der Konfigurationsmenge deployt, daher gilt derselbe Tag- Filter. Der Recovery Services Vault ist der Standard und der Backup Vault ist das Opt-in, deshalb werden beide kombiniert.

recoveryservicesresources
| where type =~ 'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'
| extend
    vmname = tostring(properties.friendlyName),
    protectionState = tostring(properties.protectionState),
    lastBackupStatus = tostring(properties.lastBackupStatus),
    lastBackupTime = todatetime(properties.lastBackupTime),
    policyName = tostring(properties.policyName),
    vault = tostring(split(id, '/', 8)[0])
| project vmname, vault, policyName, protectionState, lastBackupStatus, lastBackupTime,
    subscriptionId, resourceGroup
| union (
    recoveryservicesresources
    | where type =~ 'microsoft.dataprotection/backupvaults/backupinstances'
    | extend
        vmname = tostring(properties.friendlyName),
        protectionState = tostring(properties.currentProtectionState),
        lastBackupStatus = tostring(properties.protectionStatus.status),
        lastBackupTime = todatetime(properties.latestRecoveryPointTime),
        policyName = tostring(properties.policyInfo.name),
        vault = tostring(split(id, '/', 8)[0])
    | project vmname, vault, policyName, protectionState, lastBackupStatus, lastBackupTime,
        subscriptionId, resourceGroup
)
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| join kind=inner (
    resourcecontainers
    | where type =~ 'microsoft.resources/subscriptions/resourcegroups'
    | where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
    | project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
) on rgId
| project vmname, vmConfiguration, vault, policyName, protectionState,
    lastBackupStatus, lastBackupTime
| order by vmname asc

7. Ausstehende Patches

Das Patch-Dashboard zeigt die letzte Patch-Installation. Diese Abfrage zeigt, was noch aussteht, also die andere Hälfte des Bildes.

patchassessmentresources
| where type =~ 'microsoft.compute/virtualmachines/patchassessmentresults'
| extend
    vmname = tostring(split(id, '/', 8)[0]),
    assessmentTime = todatetime(properties.startDateTime),
    osType = tostring(properties.osType),
    rebootPending = tostring(properties.rebootPending),
    criticalAndSecurityPatchCount = toint(properties.criticalAndSecurityPatchCount),
    otherPatchCount = toint(properties.otherPatchCount)
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| join kind=inner (
    resourcecontainers
    | where type =~ 'microsoft.resources/subscriptions/resourcegroups'
    | where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
    | project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
) on rgId
| where vmname in (${vms})
| summarize arg_max(assessmentTime, *) by vmname
| project vmname, vmConfiguration, assessmentTime, osType, rebootPending,
    criticalAndSecurityPatchCount, otherPatchCount
| order by criticalAndSecurityPatchCount desc

8. Netzwerk-Exposition

Keine Managed VM hat eine öffentliche IP. Diese Abfrage liefert die Managed VMs, die doch eine haben, das Ergebnis sollte also leer sein.

resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
| join kind=inner (
    resources
    | where type =~ 'microsoft.network/networkinterfaces'
    | extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
    | mv-expand ipConfig = properties.ipConfigurations
    | extend
        publicIpId = tostring(ipConfig.properties.publicIPAddress.id),
        attachedTo = tostring(split(tostring(properties.virtualMachine.id), '/', 8)[0])
    | where isnotempty(publicIpId) and isnotempty(attachedTo)
    | project rgId, nic = name, publicIpId, attachedTo, resourceGroup, subscriptionId
) on rgId
| project vmname = attachedTo, vmConfiguration, nic, publicIpId, resourceGroup, subscriptionId

9. Letzte Änderungen an den Managed VMs

Resource Graph hält die Änderungshistorie vor. Ohne den Tag-Filter liefert die Abfrage Änderungen im gesamten Tenant, der Join beschränkt sie also auf den verwalteten Bestand.

resourcechanges
| extend
    changeTime = todatetime(properties.changeAttributes.timestamp),
    changeType = tostring(properties.changeType),
    targetId = tostring(properties.targetResourceId),
    targetType = tostring(properties.targetResourceType),
    changedBy = tostring(properties.changeAttributes.changedBy)
| where changeTime > ago(30d)
| where targetType =~ 'microsoft.compute/virtualmachines'
| extend
    resourceName = tostring(split(targetId, '/', 8)[0]),
    rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| join kind=inner (
    resourcecontainers
    | where type =~ 'microsoft.resources/subscriptions/resourcegroups'
    | where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
    | project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
) on rgId
| project changeTime, vmname = resourceName, vmConfiguration, changeType, changedBy, targetId
| order by changeTime desc

Ändere targetType, um über einen anderen Ressourcentyp zu berichten, oder streiche diese Zeile, um jede Änderung innerhalb der verwalteten Konfigurationsmengen zu sehen.

10. Software-Inventar innerhalb der Maschine

Change Tracking & Inventory schreibt in den Log-Analytics-Workspace. Diese Abfrage läuft auf der Azure-Monitor-Datenquelle im Modus Logs, nicht in Resource Graph.

Das Modul deployt pro VM-Konfigurationsmenge einen Workspace, der Workspace-Scope ist also schon der verwaltete Bestand und ein Tag-Filter ist nicht nötig. let ist hier verfügbar, anders als in Resource Graph.

ConfigurationData
| where ConfigDataType == "Software"
| summarize arg_max(TimeGenerated, *) by Computer, SoftwareName, CurrentVersion
| project Computer, SoftwareName, CurrentVersion, Publisher, SoftwareType, TimeGenerated
| order by Computer asc, SoftwareName asc

Ersetze Software durch WindowsServices, Daemons, Files oder Registry für die anderen Inventartypen. ConfigurationChange enthält dieselben Daten als Änderungshistorie statt als aktuellen Zustand.