Back Back
to to
Stories Stories

Apr 12th 2024

Azure Change Analysis – find the reason that broke your app

hero-image

Azure Change Analysis enhances the visibility of changes made to Azure resources. It does this by tracking these changes at the subscription level and recording them in the Azure Resource Graph’s resourceschanges table.

As of March 2024, change tracking now also includes detailed information about the principal that initiated the change, the client type (e.g. Azure Portal, Azure CLI, ARM template), and the operation which resulted in the change (e.g. Microsoft.Web/sites/write). This enhancement means you no longer need to consult Azure activity logs separately to understand who initiated a change and what action they performed. Everything is conveniently available within the Change Analysis.

The structure of the resourceschanges table can make it challenging to clearly view resource and property changes at a glance. However, you can overcome this with some Kusto magic. Below, i’ve provided an Azure Resource Graph query that simplifies these details into a more digestible table format, including all pertinent change details.

  • resourceChangeType indicates whether this is an create, update or delete of the resource
  • and for resource updates propertyChangeType indicates whether this is an insert, update or removal of the resource property
  • the propertyName as well as it’s previousValue and newValue
  • changedBy is the name or object id of the principal the initiated the change
  • operation is the Azure ARM operation which resulted in the change
  • correlationId can be used to lookup related changes and events from the Azure activity log
  • activityLogLink is link to the Azure Portal activity log prefiltered to the change event correlationId and timestamp
resourcechanges
| extend timestamp = todatetime(properties.changeAttributes.timestamp)
| extend changedBy = tostring(properties.changeAttributes.changedBy)
| extend clientType = tostring(properties.changeAttributes.clientType)
| extend operation = tostring(properties.changeAttributes.operation)
| extend correlationId = tostring(properties.changeAttributes.correlationId)
| extend resourceChangeType = tostring(properties.changeType)
| extend targetResourceId = properties.targetResourceId
| parse targetResourceId with '/subscriptions/' subscriptionId '/resourceGroups/' resourceGroup '/providers/' resourceProvider '/' resourceType '/' resource
| extend resourceProviderType = strcat(resourceProvider, '/', resourceType)
| extend activityLogQuery = strcat('{"query":{"subscriptions":["', subscriptionId, '"],"searchString":"', correlationId, '","timeSpan":"3","startTime":"', replace(' ', 'T', format_datetime(timestamp - 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z","endTime":"', replace(' ', 'T', format_datetime(timestamp + 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z"}}')
| extend activityLogLink = iff(correlationId=='00000000-0000-0000-0000-000000000000', '-', strcat('https://portal.azure.com/#view/Microsoft_Azure_ActivityLog/ActivityLogBlade/queryInputs~/', url_encode_component(activityLogQuery)))
| extend changes = iff(array_length(bag_keys(properties.changes))==0, dynamic({'-':dynamic({'propertyChangeType':'-'})}), properties.changes)
| mv-expand propertyName = bag_keys(changes) to typeof(string)
| extend propertyChangeType = tostring(changes[propertyName].propertyChangeType)
| extend previousValue = coalesce(changes[propertyName].previousValue, '-')
| extend newValue = coalesce(changes[propertyName].newValue, '-')
| sort by timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName
| project timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName, previousValue, newValue, operation, changedBy, clientType, correlationId, activityLogLink

Below is an example output of change analysis captured changes as a result of creating a Logic App resource (bottom of the table), updating the workflow, disabling the workflow and finally deleting the resource (top of the table):

Finally, below is a PowerShell script for listing the same change analysis information locally without going to the Azure Portal. The output can also be exported to a file or copy-pasted into Excel for further analysis.

$q=@" resourcechanges | extend timestamp = todatetime(properties.changeAttributes.timestamp) | extend changedBy = tostring(properties.changeAttributes.changedBy) | extend clientType = tostring(properties.changeAttributes.clientType) | extend operation = tostring(properties.changeAttributes.operation) | extend correlationId = tostring(properties.changeAttributes.correlationId) | extend resourceChangeType = tostring(properties.changeType) | extend targetResourceId = properties.targetResourceId | parse targetResourceId with '/subscriptions/' subscriptionId '/resourceGroups/' resourceGroup '/providers/' resourceProvider '/' resourceType '/' resource | extend resourceProviderType = strcat(resourceProvider, '/', resourceType) | extend activityLogQuery = strcat('{"query":{"subscriptions":["', subscriptionId, '"],"searchString":"', correlationId, '","timeSpan":"3","startTime":"', replace(' ', 'T', format_datetime(timestamp - 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z","endTime":"', replace(' ', 'T', format_datetime(timestamp + 3h, 'yyyy-MM-dd HH:mm:ss.fff')), 'Z"}}') | extend activityLogLink = iff(correlationId=='00000000-0000-0000-0000-000000000000', '-', strcat('https://portal.azure.com/#view/Microsoft_Azure_ActivityLog/ActivityLogBlade/queryInputs~/', url_encode_component(activityLogQuery))) | extend changes = iff(array_length(bag_keys(properties.changes))==0, dynamic({'-':dynamic({'propertyChangeType':'-'})}), properties.changes) | mv-expand propertyName = bag_keys(changes) to typeof(string) | extend propertyChangeType = tostring(changes[propertyName].propertyChangeType) | extend previousValue = coalesce(changes[propertyName].previousValue, '-') | extend newValue = coalesce(changes[propertyName].newValue, '-') | sort by timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName | project timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName, previousValue, newValue, operation, changedBy, clientType, correlationId, activityLogLink "@;$c=@();$r=$null;do{if($null -eq $r.skip_token){$r=$q|az graph query -q '@-'|convertfrom-json}else{$r=$q|az graph query -q '@-' --skip-token $r.skip_token|convertfrom-json};$c+=$r.data;}until($null -eq $r.skip_token);$c|select timestamp, subscriptionId, resourceGroup, resourceProviderType, resource, resourceChangeType, propertyChangeType, propertyName, previousValue, newValue, operation, changedBy, clientType, correlationId, activityLogLink|ogv -t 'Azure Resource Changes';

Came this far? Why not say Hi!

Teemu Tapanila

Teemu Tapanila

CTO, Principal Architect

teemu.tapanila@mallow.fi

+358 452 135 655

Riku Pilli

Riku Pilli

Sales Manager

riku.pilli@mallow.fi

+358 40 725 0888