Automatic notification of the latest AKS version in Azure with an Azure Logic app and an Azure Function

New releases of Kubernetes follow each other in rapid succession. Azure must support the version of Kubernetes in order to also offer it with AKS.
I would like to know when a new version of Kubernetes will be supported in AKS. This can be checked manually with the Azure CLI. However, I do not want to do this manually every now and then. That’s why I automated the process that checks the latest version of Kubernetes in AKS. The process tweets a message when a new version has been released. In addition, the process also notifies via twitter when a new location in Azure supports AKS. The proces is automated by serverless resources in Azure: with an Azure Function and an Azure Logic app. The twitter account @azureaksupdates notifies about the latest version and latest locations of AKS.

Manually checking latest version

Let’s first see how to check for the latest version manually. This can be done with the Azure CLI:

az aks get-versions --location westeurope

The result is a large json object where you have to interpret the latest version manually.

Automating the manual process

Use Fiddler to see the Azure REST API calls from the Azure CLI

I wanted a process that checks every hour or so if a new version is available. An Azure Functions seems a good fit for what I wanted. But how to run a CLI statement in an Azure Function? That’s not possible. But the CLI is actually calling the Azure REST API. I couldn’t found the get-versions in the documentation for AKS. And before executing a call I needed to authenticate with a Service Principal. In the Azure CLI this looks like this:

az login --service-principal --username "1234abcd-20b1-4919-600a-2f8d35c5ad22" --password "Pa$$word1" --tenant "1485cdbb-3b95-4277-bd62-77e94847db9e"

With a tool like Fiddler it’s possible to see what happens when a command is executed on the Azure CLI.

Before doing this you have to add two Environment Variables: Execute:

set ADAL_PYTHON_SSL_NO_VERIFY=1
set AZURE_CLI_DISABLE_CONNECTION_VERIFICATION=1

Now you can see how a request and response looks like in Fiddler when an Azure CLI command is executed.

fiddler

I’ve used Postman to execute the http calls to the Azure REST API and to investigate what is really needed regarding headers.

postman

Create the Azure Function

Now I know what HTTP calls are made. I just have to mimic this in code. Therefor I’ve created an Azure Function with type Http trigger. I’ve created the following class to call the Azure REST API:

using Newtonsoft.Json.Linq;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
 
namespace AksCheckNewVersion
{
    public static class AzureApi
    {
        private static HttpClient httpClient = new HttpClient();
 
        private const string AuthenticateURI = "https://login.microsoftonline.com/{0}/oauth2/token";
        private const string AuthenticateBody = "grant_type=client_credentials&client_id={0}&resource=https%3A%2F%2Fmanagement.core.windows.net%2F&client_secret={1}";
 
        public static async Task<string> GetAuthorizationToken()
        {
            string tenantId = Settings.GetSetting(Settings.TenantId);
            string applicationId = Settings.GetSetting(Settings.ApplicationId);
            string password = WebUtility.UrlEncode(Settings.GetSetting(Settings.ServicePrincipalPassword));
 
            string authenticateRequestUri = string.Format(AuthenticateURI, tenantId);
            string authenticateBody = string.Format(AuthenticateBody, applicationId, password);
            string authenticateContentType = "application/x-www-form-urlencoded";
 
            httpClient.DefaultRequestHeaders.Accept.Clear();
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(authenticateContentType));
 
            using (HttpContent body = new StringContent(authenticateBody))
            {
                body.Headers.ContentType = new MediaTypeHeaderValue(authenticateContentType);
 
                using (HttpResponseMessage response = await httpClient.PostAsync(authenticateRequestUri, body))
                using (HttpContent content = response.Content)
                {
                    Task<string> result = content.ReadAsStringAsync();
                    JObject jsonBody = JObject.Parse(result.Result);
                    string token = jsonBody.GetValue("access_token").ToString();
                    return token;
                }
            }
        }
 
        public static async Task<string> GetAksLocations(string token, string subscriptionId)
        {
            var providersURI = $"https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.ContainerService?api-version=2018-02-01";
            var json = await ExecuteGetOnAzureApi(providersURI, token);
            return json;
        }
 
        public static async Task<string> GetAksVersions(string token, string subscriptionId, string location)
        {
            var aksVersionsUri = $"https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.ContainerService/locations/{location}/orchestrators?api-version=2017-09-30&resource-type=managedClusters";
            var json = await ExecuteGetOnAzureApi(aksVersionsUri, token);
            return json;
        }
 
        private static async Task<string> ExecuteGetOnAzureApi(string uri, string token)
        {
            httpClient.DefaultRequestHeaders.Clear();
            httpClient.DefaultRequestHeaders.Add("Authorization", String.Format("Bearer {0}", token));
 
            using (HttpResponseMessage response = await httpClient.GetAsync(uri))
            using (HttpContent content = response.Content)
            {
                return await content.ReadAsStringAsync();
            }
        }
    }
}

This is how to get the latest version of AKS from the json:

public static Version GetLatestVersion(string json)
{
    dynamic dynJson = JsonConvert.DeserializeObject(json);
 
    List<Version> versions = new List<Version>();
 
    foreach (var item in dynJson.properties.orchestrators)
    {
        string orchestratorVersion = item.orchestratorVersion.ToString();
        versions.Add(new Version(orchestratorVersion));
    }
 
    return versions.Max();
}

Get the Azure locations that support AKS

The code above works for a single location only. So the next step is to fetch all supported locations by AKS from the REST interface. Let’s do it from the Azure CLI first. This can be done with:

az provider show --namespace Microsoft.ContainerService

The result is a large json again. So let’s filter it on managedcluster only, and return the locations only:

az provider show --namespace Microsoft.ContainerService --query "resourceTypes[?resourceType=='managedClusters'].locations | [0]"

When you execute the command above and are watching Fiddler, you will see that the responsebody is exactly the same from the Azure REST API for both commands. So Filtering is executed client side. Then we have to filter in code also. We can use some jsonpath to filter:

public static IEnumerable<string> GetLocationsWhichSupportAKS(string jsonForContainerService)
{
    JObject providerJson = JObject.Parse(jsonForContainerService);
    return providerJson.SelectToken("$..resourceTypes[?(@.resourceType=='managedClusters')].locations").Children().Select(l => l.Value<string>());
}

The code to call the REST API to get all locations which offer AKS can be found above in class AzureApi.

The Azure Function needs to store the latest version of AKS, so it can compare this version with the fetched version via the Azure REST API. I’ve used Azure Table Storage for this.

Deploying the Azure Function to Azure

In this first iteration I just publish the Azure Function in Visual Studio to Azure. After configuring the Application Settings which are needed, the function was not working.

This exception came up:

The following 1 functions are in error:
Run: Microsoft.Azure.WebJobs.Host: Error indexing method ‘AksCheckNewVersion.Run’. Microsoft.Azure.WebJobs.Host: Cannot bind parameter ‘log’ to type TraceWriter. Make sure the parameter Type is supported by the binding. If you’re using binding extensions (e.g. ServiceBus, Timers, etc.) make sure you’ve called the registration method for the extension(s) in your startup code (e.g. config.UseServiceBus(), config.UseTimers(), etc.).

It appeared the Azure Function was not running on the .NET Core runtime. After changing the Application Setting FUNCTIONS_EXTENSION_VERSION from ~1 to beta the Azure Function worked like a charm.

How the result looks like

The json result of the Azure Function looks like this:

[
   "New location UK South available in Azure supporting AKS version 1.10.3",
   "Location West Europe in Azure has a new version of AKS available: 1.10.3"
]

The json contains the direct messages that I want to send via twitter.
To send the messages to twitter, I don’t want to think about how to write the code to authenticate en send a tweet. An Azure Logic App already contains a connector for Twitter. So let’s use a Logic App.

Create the Logic App

Creating the Logic App was only a couple of minutes work.
The trigger activity is a Recurrance activity which is configured to run once an hour.

logic recurrence

When the Recurrance activity is triggered the next step is to call the Azure Function. After choosing activity Azure Functions a list of all Azure functions is shown and I had to choose the correct one. Configure a GET request. So I didn’t had to configure the master key to access the Azure Function. This is all done by the Logic App.

logic function

I chose to send a tweet per update. Therefor I need to iterate through the messages in the returned json. To make this possible first interpret the json with a Parse Json activity. This is actually a very powerfull activity. The content is configured by using the Body of the Azure Function. By clicking on “Use sample payload to generate scheme” I had to pass the result of the json and the schema is generated for me.

logic parse json

Now it’s possible to iterate over the items in the response json. So I’ve added a For Each activity. The Body of the Parse Json is used as source.

Within the For Each I’ve added a Send Email activity to send an email to myself. And also the Post a Tweet activity. The tweet text is the Current Item of the For Each.

logic foreach

With these 2 serverless resources I reached my goal. We know what the latest version of AKS is in a location and we also know when a new location in Azure supports AKS.

The whole solution can be found on my github.

TODO:

  • Find out if it’s possible to use Dependency Injection in Azure Functions to better allow unit tests
  • A CI/CD pipeline to publish the solution to Azure
  • Get notifications when an Exception occures.
Advertenties

Geef een reactie

Vul je gegevens in of klik op een icoon om in te loggen.

WordPress.com logo

Je reageert onder je WordPress.com account. Log uit /  Bijwerken )

Google+ photo

Je reageert onder je Google+ account. Log uit /  Bijwerken )

Twitter-afbeelding

Je reageert onder je Twitter account. Log uit /  Bijwerken )

Facebook foto

Je reageert onder je Facebook account. Log uit /  Bijwerken )

Verbinden met %s