Post-Image

Exploring Azure Custom Actions Part 2

In part 1 we discussed the overall architecture and use case for building custom Azure actions. We got as far as creating an ARM template that had a set of resources for us to start controlling with actions. In this part, we explore building a “management” function that will execute our restart code for us.

Custom Action Backend

There are likely a few different ways we could host this management code. In my opinion, we probably want our control/management plane to live outside of the applications itself. Further, these custom actions likely lend itself to a serverless pattern, using Azure Functions. In my case, I’m going to create an Azure Function in C#.

Based on the action endpoint documentation we can see that the Azure API will effectively make a POST request to our endpoint of type JSON, and will have properties just laid out in the body itself. For this particular call, restart, we likely don’t need any properties to be passed.

From an Azure Function perspective, we are going to want to restart the other web apps in our resource group. This will require us to have the appropriate permissions to the resource group itself, and then a list of resources for which we should take action on. For the first part, we will make use of Azure Managed Identity. For the second part, we could make use of the Azure Resource Graph to query for resources, or we could keep it simple and expect that application_settings are configured that tell the application which resources to restart. For the purposes of this blog, we will go with the latter.

So, from an infrastructure perspective, we will require the following:

  • An Azure function, running on a consumption plan
  • System assigned managed identity for the Azure Function
  • Application_settings configured that point to the resources to be restarted
  • Appropriate role assignments for the managed identity to restart the target resources

We can accomplish the infrastructure parts by extending the ARM template, which we will tackle in a later section. From a code perspective, we will require the following:

  • A single Azure function that restarts the target web applications

One important thing to note is that Azure Function URLs do not populate until “code” has been deployed to the function. What this means is that we cannot create the Azure Custom Action until our function code has been deployed. Since the plan is to have this all in one ARM template, we will need to have the code written, compiled, and zipped up prior to deploying our ARM template. We will make use of the Azure App Service: Run As Package functionality to make this work. Don’t worry if you don’t quite understand this step, it’ll make more sense as we work through the next parts.

First thing we will do is create a service class in our Azure Function to handle the restarting of our web applications. You can use the built-in new project wizard in Visual Studio 2022 to create an Azure Function template. Just be sure to select .NET 6, which is the current LTS. There are a couple of different ways we can use to interact with Azure Resources. For this example, I’ve chosen to work directly against the REST api. As such, I need to obtain a token credential (using the Azure.Identity package) and then basically set that in my http client. Have a look at the following code:

using Azure.Core;
using Azure.Identity;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace CustomAction.services
{
    public class AzureManagement
    {
        private HttpClient _httpClient;

        public AzureManagement()
        {
            _httpClient = new HttpClient();
            _httpClient.BaseAddress = new Uri("https://management.azure.com/");
            var tokenCredential = new DefaultAzureCredential();
            var accessToken = tokenCredential.GetToken(
                new TokenRequestContext(scopes: new string[] { "https://management.azure.com/.default" }) { }
            );
            _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken.Token);
        }

        public async Task RestartAppService(string appServiceResourceId)
        {
            var url = appServiceResourceId + "/restart?api-version=2021-02-01";
            var requestMessage = new HttpRequestMessage(HttpMethod.Post, url);
            var response = await _httpClient.SendAsync(requestMessage);
            if (!response.IsSuccessStatusCode) { throw new Exception("Failed to restart web app." + response.ReasonPhrase); }
        }
    }
}

In order to make this service available to my function code, I’ll modify the HostBuilder to use dependency injection to surface this as a singleton. For example:

using CustomAction.services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CustomAction
{
    public class Program
    {
        public static void Main()
        {
            var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults()
                .ConfigureServices(services =>
                {
                    services.AddSingleton(new AzureManagement());
                })
                .Build();

            host.Run();
        }
    }
}

And lastly, the function code itself.

using System;
using System.Net;
using System.Threading.Tasks;
using CustomAction.services;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

namespace CustomAction
{
    public class Actions
    {
        private readonly ILogger _logger;
        private readonly AzureManagement _azureManagement;

        public Actions(ILoggerFactory loggerFactory, AzureManagement azureManagement)
        {
            _logger = loggerFactory.CreateLogger<Actions>();
            _azureManagement = azureManagement;
        }

        [Function("Restart")]
        public async Task<HttpResponseData> Restart([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
        {
            _logger.LogInformation("Starting restart function");

            try
            {
                var webApp1Id = System.Environment.GetEnvironmentVariable("WEBAPP1_ID");
                var webApp2Id = System.Environment.GetEnvironmentVariable("WEBAPP2_ID");
                await _azureManagement.RestartAppService(webApp1Id);
                await _azureManagement.RestartAppService(webApp2Id);
                return req.CreateResponse(HttpStatusCode.OK);
            }
            catch(Exception e)
            {
                _logger.LogError(e, "Whooooooops");
                return req.CreateResponse(HttpStatusCode.InternalServerError);
            }
        }
    }
}

Because I am making use of Azure.Identity and the DefaultCredential pattern, I can actually attempt to test this locally with my currently deployed resources to make sure that this works. This will require me to setup Azure Service Authentication and have an appropriate local.settings.json file. You can find the resource id’s of the web applications by navigating to those resources in the portal and clicking on the properties tab. For example:

Starting my function in Visual Studio 2022, I can test with postman by making a POST call to http://localhost:7071/api/Restart. Here is what you can expect to see in the functions output.

In order to confirm that the code worked, you can have a look at the activity log for one of the web applications deployed. You will see something similar to:

The Restart Web App activity log entry shows that our code worked successfully! The next steps, which include deploying code and updating our ARM template, we will tackle in part 3.

 

About Shamir Charania

Shamir Charania, a seasoned cloud expert, possesses in-depth expertise in Amazon Web Services (AWS) and Microsoft Azure, complemented by his six-year tenure as a Microsoft MVP in Azure. At Keep Secure, Shamir provides strategic cloud guidance, with senior architecture-level decision-making to having the technical chops to back it all up. With a strong emphasis on cybersecurity, he develops robust global cloud strategies prioritizing data protection and resilience. Leveraging complexity theory, Shamir delivers innovative and elegant solutions to address complex requirements while driving business growth, positioning himself as a driving force in cloud transformation for organizations in the digital age.

Share This Article

Comments