9 min read

Azure Resource Change Reporting using the Resource Change History API, Azure Functions and Blazor

Azure Resource Change Reporting using the Resource Change History API, Azure Functions and Blazor

On my journey to becoming better at developing .Net Core solutions I was looking for a project. I played with the Change History API before and decided I wanted to turn this into a .Net Core solution. Long story short: I build something.

The Resource Change History API is still in preview and we can see that from the results. Not all resource report changes so that doesn't necessarily make it the most dependable solution. But if you look at what does report change this can be a quite useful tool.

Long story short, code here: https://github.com/whaakman/azure-resource-changes-reporting

The Requirements

Because the Resource Change History API can report quite some data I decided that I only wanted to report changes for resources that I tagged and I only wanted to report changes made by the user. System changes (done by Azure) often include the property "lastModifiedTimeUtc" that changed. When you change something, it's possible that Azure itself updates that property. Not necessarily something I want to see.

To achieve my goal I decided to write an Azure Function and then use an App Service to build the report (basically just a table).

My initial setup retrieves the changes for resources with tag "ReportChanges":"true" for the past 14 days that the Resource Change History API holds. I only wanted the changed properties and was not interested in the difference (Before and After Snapshot). As Azure has a beautiful GUI for building the diff, there is no point in doing this myself.

To create an overview of the changed properties I needed the following:

  • Azure Function
    Trigger on an HTTP Request
    Get all the Resource Ids with a specific tag
    Get all the changed properties for a resource
    Return the results
  • Azure App Service
    Call the Azure Function
    Report the Resources and the changed properties

The Solution

I'm not going through all lines of code as it's quite some code but some things are worth taking a better look at.

The Azure Function
In the Azure Function we basically want to achieve two things: Get the Resource IDs with the tag "ReportChanges":"True" and for all those Resourse IDs, return the changes.

For all this to work we need a token to authenticate with Azure. I used Managed Identity which means we can use "Microsoft.Azure.Services.AppAuthentication" and retrieve a token.

public static async Task<string> GetToken()
{
     // Get the access token from the managed identity
      var azureServiceTokenProvider = new AzureServiceTokenProvider();
      string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://management.azure.com");

      return accessToken;
}

Once we have a token we can focus on getting the Resource Ids. We can achieve this by using an HTTP Client to call "https://management.azure.com/subscriptions/<SUBSCRIPTIONID>/resources?$filter=tagName eq 'ReportChanges' and tagValue eq 'true'&api-version=2020-06-01". This will return all the Resource Ids that match the tag.

{
    "value": [
        {
            "id": "/subscriptions/<SUBSCRIPTIONID>/resourceGroups/Demo-WebApp-ISV1011/providers/Microsoft.Sql/servers/sqlserverd4ffyq63veqp4",
            "name": "sqlserverd4ffyq63veqp4",
            "type": "Microsoft.Sql/servers",
            "kind": "v12.0",
            "location": "westeurope"
        },
        {
            "id": "/subscriptions/<SUBSCRIPTIONID>/resourceGroups/Demo-WebApp-ISV1011/providers/Microsoft.Web/sites/APP-isvdemo1011",
            "name": "APP-isvdemo1011",
            "type": "Microsoft.Web/sites",
            "kind": "app",
            "location": "westeurope"
        },

    ]
}

This response is not always identical. Resources often have additional properties (like SKU). This made it hard (for me) to deserialize it directly to an object. For this I used LINQ to get the property I needed, the "id".

JObject results = JObject.Parse(HttpsResponse);

            // Store all resource Ids in list
            var resourceIds =
                    from id in results["value"]
                    select (string)id["id"];    
List<string> resourceIdsList = new List<string>();
            foreach (string item in resourceIds )
            {
                resourceIdsList.Add(item);
        
            }

Once I had that done, you can loop through the List of Resource IDs (resourceIdsList) and perform another action, like getting all the changes.

To get the changes we need to call the Resource Changes API at "https://management.azure.com/providers/Microsoft.ResourceGraph/resourceChanges?api-version=2018-09-01-preview".

When the API was first released you could only get the before snapshot and after snapshot and then had to build your own diff (read https://www.wesleyhaakman.org/automating-change-detection/). But you no longer have to do that! The Resource Changes API can also return just the changed properties (documentation here). All we need to do is add the property "fetchPropertyChanges": true to the request body. Sounds like something we need!

The body needs to be formatted like so:

{
    "resourceId": "/subscriptions/{subscriptionId}/resourceGroups/MyResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
    "interval": {
        "start": "2019-09-28T00:00:00.000Z",
        "end": "2019-09-29T00:00:00.000Z"
    },
    "fetchPropertyChanges": true
}


The challenge here is providing the interval (start and end) as these need to be in the Zulu timezone (Z). All changes that occurred within this window will be returned. I solved that using the following DateTime annotation:

 var startDateTime = DateTime.Now.AddDays(-14).ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'").ToString();
            var endDateTime = DateTime.Now.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'").ToString();

Next we need the the URI and then loop through all the Resource Ids we retrieved earlier.

 string URI = "https://management.azure.com/providers/Microsoft.ResourceGraph/resourceChanges?api-version=2018-09-01-preview";

            foreach (var resourceId in resourceIds)
            {

                // Build the body for the POST
                string str = $"{{ 'resourceId': '{resourceId}', 'interval': {{ 'start': '{startDateTime}', 'end': '{endDateTime}' }}, 'fetchPropertyChanges': true }}";
                var json  = JsonConvert.DeserializeObject(str);

                // Perform POST, store results in HttpsResponse
                HttpResponseMessage responsePost = await httpClient.PostAsJsonAsync(URI, json);
                var HttpsResponse = await responsePost.Content.ReadAsStringAsync();

............

This will return all the changes for the resources in JSON like below. As we can see there can be multiple changes with quite some values. What we're interested in is the "propertyChanges".

{
    "changes": [
        {
            "changeId": "{\"beforeId\":\"e17c94b9-d8fe-4b43-82c7-3af5a0fb0bb4\",\"beforeTime\":\"2020-11-12T20:46:51.428Z\",\"afterId\":\"ddf5cb7c-7bad-4f91-8d39-b8af2b9dfb10\",\"afterTime\":\"2020-11-12T20:55:49.609Z\"}",
            "beforeSnapshot": {
                "snapshotId": "e17c94b9-d8fe-4b43-82c7-3af5a0fb0bb4",
                "timestamp": "2020-11-12T20:46:51.428Z"
            },
            "afterSnapshot": {
                "snapshotId": "ddf5cb7c-7bad-4f91-8d39-b8af2b9dfb10",
                "timestamp": "2020-11-12T20:55:49.609Z"
            },
            "propertyChanges": [
                {
                    "propertyName": "properties.supportsHttpsTrafficOnly",
                    "beforeValue": "False",
                    "afterValue": "True",
                    "changeCategory": "User",
                    "changeType": "Update"
                }
            ],
            "changeType": "Update"
        },
        {
            "changeId": NEXT CHANGE etc.
            
    ]
}

First we need to deseralize this into something we can work with. Though, you don't really have to but I really liked doing it as it made it easier for me to work with the objects later. To do this I created a class that contains all the properties that I can later instantiate and create a list of.

To build a class from that Json is quite some typing but then I learned about https://json2csharp.com! This created the class I needed in order for me to build my list and deserialize the Json. After creating that class all we needed to do is deseralize the Json.

RootChanges DeserializedClass = JsonConvert.DeserializeObject<RootChanges>(HttpsResponse);

We then need to create a new class for an object that can hold the properties we need. I wanted to report the properties:

  • Resource Id
  • propertName
  • beforeValue
  • afterValue
  • changeCategory
  • timestamp

I created a class for that "ChangeProperties.cs". And then looped through all changes in my Deserialized Class (RootChanges) . I admit, I probably could've done it with less foreach loops but that's for the next version :)

foreach (var detectedChange in DeserializedClass.Changes)
                {
                    foreach (var propertyChange in detectedChange.PropertyChanges)
                    {
                        if (propertyChange.ChangeCategory != "System")
                        {
                            var timestamp = detectedChange.AfterSnapshot.Timestamp;
                            ChangeProperties.Add(new ChangeProperties(resourceId, propertyChange.PropertyName, propertyChange.BeforeValue, propertyChange.AfterValue, propertyChange.ChangeCategory, detectedChange.AfterSnapshot.Timestamp.ToString()));
                        }
                    }
                }

Now all there's left is to put everything together and return the result.

var APICall = new APICall();

var accessToken = await APICall.GetToken();

List<ChangeProperties> ChangeProperties = await APICall.GetChanges(accessToken, subscriptionId);

return new OkObjectResult(ChangeProperties);

Great! Now we can use something like an App Service to request the data and present it.

The App Service

For the Frontend I went with Blazor. I had never used it before so this was quite challenging but fortunately I made it. Special thanks to Gregor Suttie for helping me debug this and be the listener to my ramblings and frustrations.

In the Blazor App we also need a ChangeProperties class like we do in the Azure Function in order to build the list of changed properties.

ChangeProperties.cs


namespace ChangeHistoryWebApp
{

  public class ChangeProperties   
  {
        public string ResourceId { get; set; } 
        public string PropertyName { get; set; } 
        public string BeforeValue { get; set; } 
        public string AfterValue { get; set; } 
        public string ChangeCategory { get; set; } 
        public string Timestamp { get; set; }
   }
}

I ran into some challenges with Blazor. The biggest one being the use of the "GetJsonAsync" method in the HttpClient. To use this we need to add the package "Microsoft.AspNetCore.Blazor.HttpClient" version 3.0.0. This worked locally but when publishing this to a Web App in multiple ways this resulted in a catastrophical failure . Eh.. Time to change things. Instead of using the "GetJsonAsync" method I deserialized the conents just like I did in the Azure Function.

Changes.cs

 public class Changes
    {
        public async Task<List<ChangeProperties>> GetChangeProperties(string subscriptionId)
        {
        string azureFunctionAddress = Environment.GetEnvironmentVariable("AzureFunctionAddress");
        HttpClient client = new HttpClient();
        string URI = $"{azureFunctionAddress}&subscriptionId={subscriptionId}";
        HttpResponseMessage response = await client.GetAsync(URI);

        var detectedChanges = JsonConvert.DeserializeObject<List<ChangeProperties>>(
            await response.Content.ReadAsStringAsync());
            return detectedChanges;
        }
    }

This also retrieves the App Setting for the AzureFunctionAddress. Keep in mind to configure this :)

Last but not least we to change the Index.Razor to include all the resource changes we've gathered! In the @code block we're creating a new List object of type "ChangeProperties" and populate it with the results of "Changes.GetChangesProperties"

@code {
    // This code could probably live somewhere else :)
    
    // added subscription model to later be able to change subscriptions
    private SubscriptionModel subscriptionModel = new SubscriptionModel();
    
    private List<ChangeProperties> Changes = new List<ChangeProperties>();
    Changes ChangeObject = new Changes();
   
    protected override async Task OnInitializedAsync() => await GetChangeProperties();
    private async Task GetChangeProperties() => Changes = await ChangeObject.GetChangeProperties(subscriptionModel.Id);
    }

I've used an additional class (SubscriptionModel) to store the subscription Id. This is not neccesarily something I need now but I want to add the option to change subscriptions in the interface. So this is for later use but we might as well use it now :)

public class SubscriptionModel
{
    public string Id { get; set; } = Environment.GetEnvironmentVariable("subscriptionId");
}

Like the Azure Function Address, don't forget to create a subscriptionId app setting and add your subscription id as a value.  

Last thing to do is create the table that holds our values.

    <table class="table table-striped table-bordered">
        <thead>
            <tr>
                <th><span class="sort-link" @onclick=@(() => SortTable("ResourceId"))>ResourceId</span></th>
                <th>Changed Property</th>
                <th>Before Value</th>
                <th>After Value</th>
                <th>Change Category</th>
                <th><span class="sort-link" @onclick=@(() => SortTable("Timestamp"))>Timestamp</span></th>
            </tr>
        </thead>
        <tbody>
            
            @foreach (var change in Changes)
            {
                if (String.IsNullOrEmpty(change.BeforeValue))
                {
                    change.BeforeValue = "New setting";
                }
                <tr>
                    <td>@change.ResourceId</td>
                    <td>@change.PropertyName</td>
                    <td>@change.BeforeValue</td>
                    <td>@change.AfterValue</td>
                    <td>@change.ChangeCategory</td>
                    <td>@change.Timestamp</td>
                </tr>
            }
        </tbody>
    </table>

We're looping through the Changes and show the properties in a table. For each detected change, row is created.

Additionally I made the tables sortable by using this function (https://exceptionnotfound.net/exploring-blazor-by-making-an-html-table-sortable-in-net-core/).

Wrap-up
That's it, reporting in change history using Azure Functions and Blazor. Great learning for me and hopefully you enjoyed it as well.

I'm going to try and improve this and see if I can tidy up the code and add some features like selecting different subscriptions, aggregate on a resource id (collapse and expand) and add a database to increase the history (max is 14 days using the API).

Code and description here: https://github.com/whaakman/azure-resource-changes-reporting

If you've got questions or feedback let me know!