Application Initialization with Azure Functions

TL/DR

You can warm up a function app using an applicationhost.config transform with an appliationhost.xdt. This ONLY works with Windows as application initialization is a concept built into IIS. I’m not aware of the same feature set available in Linux. Skip to the config section for the transformation setup.

Background

A couple months ago I wrote a blog about troubleshooting application initialization, a mechanism used to warm up an application after scaling or slot swap to avoid coldstart. In the case of a web application, we can use the web.config configuration to accomplish this task, but with Azure Functions the web.config is controlled by the Azure Functions “platform” configuration. This is where applicationhost.config transforms come into play.

Transforms are a mechanism used across config files that allow different configurations to be used for different environments. In our scenario we need to overwrite/modify the function apps configuration and add the application initialization information. Due to how the consumption plan works with specialization I would not expect application initialization to work as expected.

Configuration

ApplicationHost.xdt Setup

When this transform occurs, it checks to see if this entry is missing from the applicationhost.config. Since it is, the entry is added into the applicationhost.config.

https://docs.microsoft.com/en-us/previous-versions/aspnet/dd465326(v=vs.110)#using-transform-and-locator-attributes-on-separate-elements

Create a file named applicationhost.xdt in the D:/home/site directory modifying the initializationPage.

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <location path="%XDT_SITENAME%" xdt:Transform="InsertIfMissing" xdt:Locator="Match(path)">
    <system.webServer xdt:Transform="InsertIfMissing">
      <applicationInitialization xdt:Transform="InsertIfMissing">
        <add initializationPage="/api/httptrigger2"  xdt:Transform="InsertIfMissing"/>
      </applicationInitialization>
    </system.webServer>
  </location>
</configuration>

To programmatically add this file, you’ll need to use one the Kudu APIs to write the D:/home/site directory as Zip Deploy or other code deployment methods defaults to writing to the D:/home/site/wwwroot directory.

https://github.com/projectkudu/kudu/wiki/REST-API#sample-of-using-rest-api-with-curl

We can confirm that this has been correctly added to the applicationhost.config by navigating to D:\local\Temp\applicationhost.config.

If the initialization configuration is not present, try restarting the function app. If you add the file without a restart the transform will not occur.

Testing

Since application initialization is not logged in the IIS logs, I created a simple function that logged the user agent to confirm when the initialization request was first requested, thus validating if requests were routed to that instance prior to the initialization request first completing. My testing was all done in a dedicated app service plan.

Function 1

First Function representing an HTTP triggered function that takes a short period of time to execute:

#r "Newtonsoft.Json"
 
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
 
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");
    log.LogInformation("NotWarmUpPath");
 
    string name = req.Query["name"];
 
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    name = name ?? data?.name;
 
    return name != null
        ? (ActionResult)new OkObjectResult($"Not WarmUP, {name} on VM {Environment.GetEnvironmentVariable("COMPUTERNAME")}")
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}

Function 2

Second HTTP triggered function representing a longer running function that may take time to execute. This function has a 5 minute sleep configured:

#r "Newtonsoft.Json"
 
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System.Threading;
using System;
 
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");
    Thread.Sleep(360000);
    log.LogInformation($"VM {Environment.GetEnvironmentVariable("COMPUTERNAME")}");
    log.LogInformation($"Headers {req.Headers["user-agent"]}");
 
    string name = req.Query["name"];
 
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    name = name ?? data?.name;
 
    return name != null
        ? (ActionResult)new OkObjectResult($"Hello, {name}")
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}

Batch Script to Call the function

:loop
echo %date% %time% >> output.txt
curl http://jebrook-functionapp.azurewebsites.net/api/HttpTrigger3?name=Azure >> output.txt
echo . >> output.txt
goto loop

Logging

After scaling to an additional instance from the live metric stream I filtered on the new instance and confirmed there was no activity besides the initialization between 10:51 and 10:57 as the initialization function slept for 360 seconds or 5 minutes.

A deeper dive into the traces logs in application insights confirms this as well. From the screenshot below, the green box is the tail end of the host initialization which completes in roughly 8 seconds. Next the blue box represents the application initialization request. Since the user agent was logged we can confirm that this is in fact the app init request on the new worker. Finally, the red box shows that after the initialization completed.

Conclusion

As you have tested above, we confirmed application initialization works with Azure Functions in the dedicated plan. Instead of using a web.config to configure the initialization, the applicationhost.xdt created in the D:/home/site directory is used instead.