Introduction
When administrating an Azure environment, or any environment really, one will most likely find a way to track changes that were introduced. There are a number of ways to do this. Within Azure can query the Subscription or Resource Group Deployment, the downside though is this approach is limited to just the scope you are querying on. What if this is a larger organization with multiple subscriptions? You could also rely on a well-established CI/CD pipeline, a third-party governance tool, or in this case query Azure directly via the Resource Graph Explorer.
For this blog will focus on using the Azure Portal offering of the tool; however, want to note that since this is API driven there are numerous offerings such as Azure PowerShell, Azure CLI, .NET, even Ruby.
PreReqs
- An understanding of Kusto Query Language (KQL) is very helpful
- Knowing what types of tables are available in Resource Graph
- The individual/process querying need to have read access over the available Azure resources
Limitations
- Documentation clearly supports will store only last 14 days worth of changes.
- Latest documentation shows the Resource Change History API has been in preview since 4/23/19
How
We will be querying two tables for this exercise. It is fairly straight forward; however, something anyone can further customize by joining additional tables and/or filters two. This will deal with only two tables: resources and resource changes. To access the Resource Graph Explorer type 'Resource Graph Explorer' in the Azure search bar.
resources
This table is defined as: "The default table if none defined in the query. Most Resource Manager resource types and properties are here."
What this essentially translates to is that this table will hold the basic properties of all the resources. As of this writing the here are some of the columns are available, and I am providing a brief description of what the values mean.
Column Name | Description |
id | Unique id of the Azure Resource. This will include subscription ID down to individual resource. |
name | Name of the Azure Resource |
type | The Microsoft defined type of resource. Complete list available here. |
tenantId | Azure AD Tenant associated with the resource |
kind | Not always present: however, some values could be "StorageV2" or "functionapp" for microsoft.storage/storage account or microsoft.web/sites respectively |
location | What location within azure the resource is deployed to, 'global' for those that are global. |
resourceGroup | The Azure Resource Group the resource resides in |
subscriptionId | The associated Azure Subscription |
sku | Not always present; however, what pricing tier or sku the resource has been set to |
properties | This will include the provisioning status as well as the JSON Azure Resource Manager definition of the resource |
tags | Any tags that have been applied |
resourcechanges
This is the table that is preview and whose dataset is limited to 14 days as mentioned above. This table will follow the same schema; however, the all-important information on what changed is contained in the properties as a JSON object.
See:
Query
So now we have the two tables, the question becomes how to join them together. If new I highly advise brushing up on KQL. Essentially our query will need to accomplish:
- Expanding the resourcechanges properties to retrieve change details and targetResourceId of the change
- Join resources to resourcechanges on the resourceId and targetResourceId field
- pull any additional resource information for reporting purposes.
For our purposes I'll pull the Resource Name, Resource Type, Subscription, and Resource Group Name as these could be beneficial for anyone doing reporting.
One thing that did trip me up when working with joins in KQL. All columns which will be used from the join table need to be exposed in the project command, this INCLUDES the field you are joining on.
The Query
resourcechanges
| extend changeTime = todatetime(properties.changeAttributes.timestamp), targetResourceId = tostring(properties.targetResourceId),
changeType = tostring(properties.changeType), correlationId = properties.changeAttributes.correlationId,
changedProperties = properties.changes, changeCount = properties.changeAttributes.changesCount
| where changeTime > ago(180d)
| join kind=inner (resources | project resources_Name = name, resources_Type = type, resources_Subscription= subscriptionId, resources_ResourceGroup= resourceGroup, id) on $left.targetResourceId == $right.id
| project resources_Name, resources_Type, resources_Subscription, resources_ResourceGroup, changeTime, targetResourceId, changeType, correlationId, changeCount, changedProperties
The Results
Conclusion
There you go. This may seem like a complex approach to something as easy as what's changed across my Azure subscriptions; however, this approach does accurately achieve that goal. Furthermore, with the APIs being exposed across multiple programming languages leaves upon limitless possibilities as to what one can do.