December 2018

Volume 33 Number 12

[Containers]

Using Azure Containers to Provide an On-Demand R Server

By Will Stott | December 2018

In an article in the November 2018 issue of MSDN Magazine, “Web Site Background Processing with Azure Service Bus Queues” (msdn.com/magazine/mt830371), I explained how you can use an Azure Function and a Service Bus Queue to handle long-running background processing for your Web site. In this second article, I explain how you can use such processing to start a server when traffic arrives from your Web App and then use the server to perform classification tasks before automatically shutting down the server after the Web traffic has stopped for a prescribed time. I treat the server as a black box because it’s implemented as a Docker container.

In my case the server runs Linux and a logistic regression classifier developed in the statistical language R to provide quality control data for ultrasound scans as part of my research for the United Kingdom Collaborative Trial of Ovarian Cancer Screening (UKCTOCS). Your server, however, might run Windows and Python to identify arbitrage opportunities for foreign exchange currency transactions. It really doesn’t matter what the server does, so long as it can be made into a Docker image and it has an API you can access.

The key technology you’ll encounter in this article is the Azure.Management.Fluent API, which is used to programmatically create and delete an Azure Container Instance (ACI) for your server. In addition, you’ll build on your earlier work, as described in the previous article, to create an Azure Function with a timer trigger to orchestrate these actions. You’ll also extend the existing Azure Function with the Service Bus Queue trigger so it can pass input data referenced by the queued message to the classifier running in your ACI using its OpenCPU API. In this way you’ll employ Azure server-less functions to perform meaningful long-running background tasks for an ASP.NET Core 2.1 MVC Web App. An overview of the entire system is shown in Figure 1.

System Overview
Figure 1 System Overview

Recreating the project featured in this article requires only rudimentary Web and C# development skills, but assumes that you already built the Azure Functions project described in the previous article. It also assumes that you built the simple WebApp and database project described in the companion online resource. You can find the resource PDF, related source code and instructions for building the solution shown in Figure 1 at github.com/wpqs/MSDNOvaryVis. In addition, the Docker Image containing the server’s R classifier is freely available from hub.docker.com as r/wpqs/ovaryclassifier. In terms of tools, you’ll need Visual Studio 2017 v15.7 with .NET Core 2.1 SDK and the Web development workload, as well as v15.0.404240 of the Azure Function and WebJob tools. Note that the Community version of Visual Studio is available for free. You’ll also need an Azure Subscription, but again, you can get what you need for free if you’re a new customer.

Running Your Server in an Azure Container Instance

Anyone who has provisioned a virtual machine (VM) will be familiar with the idea of loading its image from some sort of file. This image contains the OS, settings, data, application software and everything else needed by your VM. Docker allows you to do much the same thing, but more efficiently because resources are shared better between instances of images running on the same machine.

The Docker image used for my research was based on a Linux server with the R statistical software and OpenCPU installed. OpenCPU (opencpu.org) provided me with an API so my R classifier function could be invoked using HTTP messaging through the host machine’s port 80. The people at OpenCPU also helpfully provided a public Docker image that served as the base for my server. All I needed to do was add the R function and my classifier.

This ability to build up Docker images layer upon layer and reuse existing work is an important part of what makes it so attractive to developers. Azure provides support for Docker by allowing you to create an Azure Container Instance (ACI), which is like a Docker Container in that it’s an instance of a Docker image. Therefore, by creating an ACI from the image in Docker Hub, you start an instance of the server on the Azure platform and when you delete the ACI this server stops.

Preparatory Work

Before implementing the code needed to manage an ACI, it’s necessary to do some preparatory work by installing a few additional packages in the Azure Function App project developed as part of the work described in the previous article. You’ll also need to create a Security Principal for this Azure Function and update its Application Settings.

Checking the existing Azure Function App project and its packages is a good idea once you’ve opened the Visual Studio MSDN­OvaryVis Solution, either from the article’s download or as created when following the instructions in my previous article.

The key packages to check using the NuGet Package Manager are Microsoft.EntityFramework­Core.Sql­Server v2.1.2 and Microsoft.NET.Sdk.Functions v1.0.19. These packages were used for this article, but you might want to try later versions for your own work. You can find details about the use of NuGet Package Manager in the related resource PDF I’ve hosted online in the GitHub repository.

Adding the packages needed to control Azure Containers and send messages to your ServiceBus Queue just requires you to give the following commands from the package manager:

Install-Package Microsoft.Azure.Management.Fluent
  -Project OvaryVisFnApp
  -Version 1.14.0
Install-Package Microsoft.Azure.WebJobs.ServiceBus
   -Project OvaryVisFnApp   -Version 3.0.0-beta8
Install-Package Microsoft.Azure.ServiceBus
  -Project OvaryVisFnApp
  -Version 3.1.0

Adding these packages means that you need to set the project’s target framework to netstandard2.0.

Granting your Azure Function the authority it needs to create and delete an Azure Container Instance requires you to create a security principal for your Azure Subscription. The PowerShell Console built into the Azure Portal Web site, shown in Figure 2, provides an easy way to do this using the following commands, though you need to replace EEE with a suitable password and MsdnOvaryVis with the name of your own Azure subscription:

az account set --subscription MsdnOvaryVis
$password = ConvertTo-SecureString "EEE" -AsPlainText -Force
$sp = New-AzureRmADServicePrincipal -DisplayName "MSDNOvaryVisApp"
  -Password $password
New-AzureRmRoleAssignment -ServicePrincipalName $sp.ApplicationId
  -RoleDefinitionName Contributor
$sp | Select DisplayName, ApplicationId
Get-AzureRmSubscription -SubscriptionName MsdnOvaryVis

The Cloud Shell of the Azure Portal PowerShell Console
Figure 2 The Cloud Shell of the Azure Portal PowerShell Console

You should copy the responses and your password into Notepad (or similar) so you can save the infor­mation. The important values to keep are your password (EEE), App Id and Tenant Id. However, instead of hardcoding them into your code, it’s better to reference them from the Application settings of your Azure Function App service. In a production system you might consider using the new Azure Managed Service Identity (MSI) as an alternative to creating a security principal and adding it to the application settings of your Azure Function as described earlier.

Updating the Application settings of your Azure Function App allows you to avoid hardcoding changeable or sensitive values into your code. You can open its Application Settings blade in the Azure Portal and then add the items in the red boxes shown in Figure 3. Alternatively, you can add settings by issuing commands from the Cloud Shell. For example, given my Function App is called MSDNOvaryVisFnApp and is located in the resMSDNOvaryVis resource group, I can give its Security Principal ID setting a value of DDD, as follows:

az functionapp config appsettings set --name MSDNOvaryVisFnApp
  --resource-group resMSDNOvaryVis --settings 'SecPrincipalId=DDD'

Figure 3 Azure Function App Service Application Settings

A complete list of the commands needed to apply the required application settings can be found in the AzureResCmds.txt file in the download supplied with this article.

Creating the Server Class

It makes sense to add a class to your Azure Function App project so you can put all the methods needed to manage your server in one place, specifically:

  • GetAzure returns an interface initialized with the security principal you created earlier.
  • GetContainerGroup finds an existing ACI running in your Azure resources.
  • IsRunning returns a Boolean to signal whether your server is currently running.
  • StartAsync creates an ACI using the specified Docker image
  • GetResult Async passes the input data to your server and return the result.
  • StopAsync deletes an ACI.

You can use Visual Studio to add an empty C# class suitable for this work by selecting your OvaryVisFnApp project, right-clicking and choosing Add | Class. This opens the Add New Item dialog box with C# Class selected. Just type Server.cs as its file name to create the corresponding Server class. Let’s walk through implementing each of these methods.

GetAzure supports the initialization of the Security Principal you created earlier, so that your code has the permissions it needs to manage resources for your Azure subscription. It’s implemented as follows:

private static IAzure GetAzure(IConfigurationRoot config)
{
  AzureCredentials credentials = SdkContext.AzureCredentialsFactory
    .FromServicePrincipal(config["SecPrincipalId"],
    config["SecPrincipalKey"], config["TenantId"],
    AzureEnvironment.AzureGlobalCloud);
  IAzure rc = Azure.Configure().WithLogLevel(
    HttpLoggingDelegatingHandler.Level.Basic)
    .Authenticate(credentials).WithDefaultSubscription();
  return rc;
}

GetContainerGroup allows you to find any container group already created in Azure, so you don’t attempt to create a new ACI when one already exists. It’s simply a matter of listing the container groups currently present in your Resource Group and returning the one with the container name specified in your application settings, or null if none are found. It’s implemented as follows:

private static async Task<IContainerGroup>GetContainerGroup(IConfigurationRoot config)
{
  IContainerGroup rc = null;
  var classifierResoureGroup = config["ClassifierResourceGroup"];
  var list = await azure.ContainerGroups.ListByResourceGroupAsync(
    classifierResoureGroup);
  foreach (var container in list){
    if((rc = await GetAzure(config).ContainerGroups
        .GetByResourceGroupAsync(
          classifierResoureGroup,config["ClassifierContainerName"])))
      break;
  }
  return rc;
}

IsRunning provides a convenient way to determine whether your server is currently running. It also sets the variable _classifierIP to the IP address of the server if running. You implement it like so:

private static string _classifierIP = "";
public static string GetIP() { return _classifierIP; }
public static async Task<bool> IsRunning(IConfigurationRoot config)
{
  _classifierIP = (await GetContainerGroup(config)).IPAddress ?? "";
  return (_classifierIP.Length > 0) ? true : false;
}

StartAsync lets you create an Azure Container Instance using the r/wpqs/ovaryclassifier Docker image to provision and start a server on-demand. It’s implemented in the Server class as shown in Figure 4. You’ll see that the server is started with TcpPort 80 open so you can communicate with it using the HTTP messaging used by OpenCPU API. You’ll also see that various parameters are passed to the method from the Azure Function’s application settings as shown in the top red box in Figure 3. These are the name and password for your Docker account, the container image, and the required specification of the ACI in terms of the minimum number of processor cores and memory size. Further information about the options for creating an Azure Container Instance can be found in Microsoft Docs at bit.ly/2CDKrJg.

Figure 4 Implementation of Server.StartAsync Starts a Server On-Demand

public static async Task<string> StartAsync(IConfigurationRoot config)
{
  string rc = "server start";
  if (await Server.IsRunning(config) == true )
    rc += " completed, already running";
  else
  {
    var region = config["ClassifierRegion"];
    var resourceGroupName = config["ClassifierResourceGroup"];
    var containerName = config["ClassifierContainerName"];
    var classifierImage = config["ClassifierImage"];
    var cpus = config["ClassifierCpus"];
    var memory = config["ClassifierMemoryGB"];
    var dockeruser = config["DockerHubUserName"];
    var dockerpwd = config["DockerHubPassword"];
    double.TryParse(cpus, out double cpuCount);
    double.TryParse(memory, out double memoryGb);
    var containerGroup =
      await GetAzure(config).ContainerGroups.Define(containerName)
        .WithRegion(Region.Create(region)).WithExistingResourceGroup(
          resourceGroupName)
        .WithLinux().WithPrivateImageRegistry("index.docker.io",
          dockeruser, dockerpwd)
        .WithoutVolume().DefineContainerInstance(containerName)
        .WithImage(classifierImage).WithExternalTcpPort(80)
        .WithCpuCoreCount(cpuCount).WithMemorySizeInGB(memoryGb)
        .Attach().WithRestartPolicy(
          ContainerGroupRestartPolicy.OnFailure).CreateAsync();
      _classifierIP = containerGroup?.IPAddress ?? “”;
    if ((_classifierIP == null) || (_classifierIP.Length == 0))
      rc += " failed";
    else
      rc += " completed ok";
  }
  return rc;
}

GetResultAsync allows you to pass data between your Azure Function and the Server. The exact implementation will depend on the way your server has implemented its API. In this case the OpenCPU API is used to pass the ovary dimensions to the classifier and get back the result—ovary visualized or not. This particular API was built to allow HTTP messaging with servers hosting R statistical functions, so it needs the following type of method:

public static async Task<int> GetResultAsync(int D1mm, int D2mm, int D3mm)
{
  int rc = -99;
  var ip = Server.GetIP();
  if (ip != "")
  {
  // Post message to server with content formed
  // from KeyValue pairs from input params
  // Check response and then query the server for the result
  // Process the result to get the return value:
  // 0 is not visualized, 1 is visualized
  }
  return rc;
}

The full implementation of GetResultAsync is available in the source-code download for this article, but it’s unlikely to be useful to you unless your server also implements the OpenCPU API.

StopAsync enables you to delete an ACI, thereby stopping the associated Azure server. This is implemented by adding a further method to the Server class, like so:

public static async Task<string> StopAsync(IConfigurationRoot config)
{
  _classifierIP = "";
  IContainerGroup containerGroup = await GetContainerGroup(config);
  if (containerGroup == null)
    return  "server stop completed already";
  else
  {
    await GetAzure(config).ContainerGroups.DeleteByIdAsync(containerGroup.Id);
    return " server stop completed ok";
  }
}

Implementing the Azure Function with Timer Trigger

An Azure Function with a periodic timer trigger is a good way to manage the starting of the server, as this operation may take a number of minutes to complete, potentially preventing timely processing of messages if performed in the Service Bus Queue Trigger Azure Function. Last month’s article described how to create the Visual Studio project you need to contain such a function. If you’re following along with the code, you just need to select the OvaryVisFnApp project, right-click to select New Azure Function and then specify the name of your new file as OvaryVisMonitor.cs. This opens the dialog box shown in Figure 5, which lets you select a Timer Trigger and set its frequency using a CRON expression—in this case, every minute. When you click OK, the dialog box closes and a new Azure Function is created in a class called OvaryVisMonitor, with a Run method appropriate for your selections.

New Azure Function Dialog Box
Figure 5 New Azure Function Dialog Box

Once you’ve created this class you should add the following code just above the Run method in order to initialize the client for your Service Bus Queue:

private static IQueueClient _queueClient = null;
private static readonly object _accesslock = new object();
private static void SetupServiceBus(string connection, string queueName)
{
  lock (_accesslock) {
    if (_queueClient == null)
      _queueClient = new QueueClient(connection, queueName);
  }
}

You’ll see that no matter how many times this SetupServiceBus method is called it only creates the Service Bus Queue client once and has a lock to make it thread safe. Therefore, you can safely do this as shown in Figure 6, knowing that even though SetupService­Bus is invoked each time your periodic timer function runs, the client will only be instantiated on the first call.

Figure 6 Implementation of the OvaryVisMonitor Timer Trigger Azure Function

[FunctionName(“OvaryVisMonitor”)]
public static async Task Run([TimerTrigger(“0 */1 * * * *”)]TimerInfo myTimer,
  TraceWriter log, Microsoft.Azure.WebJobs.ExecutionContext exeContext)
{
  using (Mutex mutex = new Mutex(true, “MSDNOvaryVisMonitorMutuex”, out bool doRun))
  {
    if (doRun == true)
    {
      var config = new ConfigurationBuilder().SetBasePath(
        exeContext?.FunctionAppDirectory)
        .AddJsonFile(“local.settings.json”, optional: true, reloadOnChange: true)
        .AddEnvironmentVariables().Build();
      var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
      optionsBuilder.UseSqlServer(
        config[“ConnectionStrings:DefaultConnection”]);
      ApplicationDbContext dbContext =
        new ApplicationDbContext(optionsBuilder.Options);
      DateTime expiry = DateTime.UtcNow.AddMinutes(-10);
      var pendingJobs = await dbContext.OvaryVis
        .Where(a => (a.ResultVis == -1) && (a.JobSubmitted > expiry)).ToListAsync();
      if (pendingJobs.Count > 0)
      {
        if (await Server.IsRunning(config) == false)
          await Server.StartAsync(config);
        else
        {
         SetupServiceBus(config[“AzureWebJobsServiceBus”],
                          config[“AzureServiceBusQueueName”]);
          foreach (var job in pendingJobs)
          {
            var message = new Message(Encoding.UTF8.GetBytes(job.Id));
            await _queueClient.SendAsync(message);
          }
        }
      }
      else
      {
        if (await Server.IsRunning(config) == true)
        {
          var recentJobs = await dbContext.OvaryVis
            .Where(a => a.JobSubmitted > expiry).ToListAsync();
          if (recentJobs.Count == 0)
            await Server.StopAsync(config);
        }
      }
    }
  }
}

You should note that your Run method needs the addition of the exeContext parameter, as shown in Figure 6. It’s used to initialize the config variable, which allows access to the Azure Function Application Settings, as provided in the earlier preparatory work. You should also note the use of the Mutex expression, which stops the body of the function being re-entered should the Run function be invoked again before its previous invocation is complete. This prevents any attempt to create a second Azure Container Instance should the startup of your server take more than a minute.

The operation of the Run function depends on obtaining a list of OvaryVis database records that have not yet been processed (that is, records with their ResultVis field set as -1). If this list isn’t empty and the server isn’t running, then Server.StartAsync is called, which blocks any further progression of the Run method code for a significant period; perhaps several minutes. Once Server.StartAsync returns, the Run method completes and the Mutex is released. Therefore, upon its next invocation, one or more messages will be sent to your Service Bus Queue—one for each record in the pending­Jobs list. This allows your pending jobs to be processed if they had been sent by OvaryVisWebApp, as described in the first article.

The final part of the timer function is concerned with stopping the server after 10 minutes of inactivity. You can see that when all the records in the OvaryVis table are at least 10 minutes old, whether processed or not, the code calls Server.StopAsync.

Updating the FormSubmittedProc method used by the OvaryVisSubmitProc Azure function completes your code implementation. You just need to edit the code created for the previous article so that the FormSubmittedProc method (called by Run) is changed, as shown in Figure 7.

Figure 7 Updated FormSubmittedProc Method in Existing Azure Function

private static async Task<string> FormSubmittedProc(IConfigurationRoot config,
  ApplicationDbContext dbContext, string queueItem)
{
  string rc = "FormSubmittedProc: ";
  if (queueItem != null)
  {
    var rec = await dbContext.OvaryVis.SingleOrDefaultAsync(a => a.Id == queueItem);
    if (rec == null)
      rc += string.Format("record not found: Id={0}", queueItem);
    else
    {
      if ((rec.ResultVis != -1) || (rec.ResultVis == -99))
        rc += string.Format("already processed: result={0}", rec.ResultVis);
      else
      {
        if (await Server.IsRunning(config) == false)
          rc += "server not running, wait for job to be resubmitted";
        else
        {
          rec.ResultVis = await Server.GetResultAsync(
            rec.D1mm, rec.D2mm, rec.D3mm);
          if (rec.ResultVis < 0)
            rc += string.Format("server running result={0} - error",
              rec.ResultVis);
          else
          {
            rc += string.Format("server running result={0} - success (ovary {1})",
                  rec.ResultVis, (rec.ResultVis == 1) ? "found" : "not found");
          }
        }
        rec.StatusMsg = rc;
        dbContext.Update(rec);
        await dbContext.SaveChangesAsync();
       }
     }
  }
  return rc;
}

Reviewing the code in Figure 7, you’ll see that FormSubmittedProc checks whether the server is running, and if so, calls GetResultAsync to obtain the result from the classifier. The database record is then updated so the result of the classifier is set in the ResultVis field. A value of 0 means ovary not visualized and 1 means ovary visualized. The StatusMsg field is also updated so the progress of the operation can be shown whenever the user refreshes the browser (F5). If the server isn’t running, the record’s StatusMsg field is updated with an appropriate message so the user knows to wait for the event to be resubmitted by OvaryVisMonitor as described earlier.

Building and publishing your updated OvaryVisFnApp project is done as described in the previous article: Right-click, select Publish and click the Publish button. However, make sure that beforehand you use the Azure Portal to stop your Azure Function App service by issuing the following command from the Cloud Shell, as shown in Figure 2:

az functionapp stop --name MSDNOvaryVisFnApp
  --resource-group resMSDNOvaryVis

Once published you need to restart the service and then functionally test it, paying particular attention to watching the Functions Server App log, as well as checking your Azure Resource Group blade where the Container Instance will appear when the server is running.

Basic functional testing can be performed by opening your browser at the WebApp’s URL as displayed in its Overview blade of the Azure Portal. Once the homepage appears, enter a set of ovary dimensions and then click Submit. You’ll immediately be redirected to the Results page, where you’ll see the values of the database record relating to your submission, as shown in the left side of Figure 8. Repeatedly pressing F5 will cause the page to be updated with the current values of this record. Eventually the results of the classifier’s assessment for these ovary dimensions will be shown, as displayed in the right side of Figure 8. If the server is already running, this will take only a few seconds, otherwise it may take a few minutes. After 10 minutes of inactivity on your Web site, the server will automatically close down, demonstrating the on-demand nature of its implementation.

Functional Test
Figure 8 Functional Test

Wrapping Up

The features of the Web site developed here are trivial. It just collects three dimensions from a form in order to produce a binary response: ovary visualized or not. However, its implementation is far from trivial. In the previous article, I showed you how to implement a way of processing the data in the background using an Azure Service Bus Queue and an Azure Function App service. In this article, I’ve extended this background processing mechanism to create an ACI from a Docker Image published on Docker Hub. This image contains a Linux server hosting R and a custom logistic regression classifier, as well as the OpenCPU API that allows communication with the server using HTTP messaging. What’s more, I implemented the logic to create this ACI on-demand, starting the server only when needed and shutting it down after 10 minutes of inactivity.

You could easily improve the system presented here by adding an Azure SignalR Service to automatically update your result Web page instead of relying on the user to periodically press F5 to refresh it. You might also consider extending the way messages are handled so that multiple ACIs are created when the Web site is experiencing high load situations. However, this extra complexity wasn’t warranted for an article that shows how to implement a server that costs just $1.00 per month (compared to the $50 a month you might pay for a virtual server operating 24/7). Even with the cost of the database service, you’re looking at paying less than $5.00 a month. That’s good value considering the technology you’re employing, but more importantly, it’s a robust solution to a real-world problem that you can build without too much effort.


Dr. Will Stott has more than 25 years of experience working as a contractor/consultant for a wide range of companies in the United Kingdom and Europe. For the last 10 years most of his time has been spent doing research at UCL on ovarian cancer screening. Dr. Stott has spoken at many conferences and is the author of papers published in various journals, as well as the book “Visual Studio Team System: Better Software Development for Agile Teams” (Addison-Wesley Professional, 2007).

Thanks to the following Microsoft technical expert for reviewing this article: Srikantan Sankaran


Discuss this article in the MSDN Magazine forum