[HOWTO] Implement and deploy a WebJob for Azure App Service using WebJobs SDK

Last week, my work colleague Guillem Bonafonte and I came across a suitable use case for WebJobs in context of a project. WebJobs is a feature of Azure App Service that allows to run a program or script on the same instance as a web app, web API, …

After consulting the documentation, we started implementing a first draft of a WebJob using WebJobs SDK v3 (Microsoft.Azure.WebJobs version 3.0.33) – WebJobs SDK v3 adds support for .NET Core. We followed the tutorial Get started with the Azure WebJobs SDK for event-driven background processing and implemented a .NET Core 6 console app inside the same solution that contains the .NET Core 6 Web API.

The different components of the resulting .NET Core 6 console app look as follows.

// appsettings.json

// Defines a custom configuration object and the time zone
{
  "CustomConfig": {
    "Test": "My test value"
  },
  "WEBSITE_TIME_ZONE": "W. Europe Standard Time"
}
// CustomConfig.cs

namespace Examples.WebJobs;

public class CustomConfig
{
    public string? Test { get; set; }
}
// Functions.cs

using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Examples.WebJobs
{
    public class Functions
    {
        // Custom configuration
        public IOptions<CustomConfig> CustomConfig { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="customConfig">custom configuration</param>
        public Functions(IOptions<CustomConfig> customConfig)
        {
            CustomConfig = customConfig;
        }

        /// <summary>
        /// Function that gets triggered every 30 seconds
        /// </summary>
        /// <param name="timerInfo">Timer trigger</param>
        /// <param name="logger">Logger</param>
        [FunctionName("ExampleFunction")]
        public void ExampleFunction([TimerTrigger("*/30 * * * * *", RunOnStartup = true)] TimerInfo timerInfo, ILogger logger)
        {
            logger.LogInformation("START ExampleFunction ...");
            Console.WriteLine(CustomConfig.Value.Test);
            logger.LogInformation("Test: " + CustomConfig.Value.Test);
            logger.LogInformation("END ExampleFunction.");
        }
    }
}

Important: Functions must be public methods and must have one trigger attribute or the NoAutomaticTrigger attribute (see here).

// Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Examples.WebJobs
{
    internal class Program
    {
        static async Task Main()
        {
            var builder = new HostBuilder();

            builder.ConfigureWebJobs((b) =>
            {
                // Seems to be required
                b.AddAzureStorageCoreServices();
                // Required to make timer trigger work
                b.AddTimers();
            });

            builder.ConfigureLogging((context, b) =>
            {
                // Adds a console logger
                b.AddConsole();
                // Grabs the app insights instrumentation key from the app service
                var instrumentationKey = context.Configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
                if (!string.IsNullOrEmpty(instrumentationKey))
                {
                    b.AddApplicationInsightsWebJobs(o => o.InstrumentationKey = instrumentationKey);
                }
            });

            builder.ConfigureServices((context, services) =>
            {
                services.Configure<CustomConfig>(context.Configuration.GetSection("CustomConfig"));
            });
            
            var host = builder.Build();
            
            using (host)
            {
                await host.RunAsync();
            }
        }
    }
}

Local execution of this WebJob worked well, so next we wanted to deploy it. As we already had a deployment pipeline (Azure DevOps YAML pipeline) in place for the deployment of the .NET Core 6 Web API, we wanted to deploy the WebJob together with the .NET Core 6 Web API. Here we first failed several times as we didn’t consider the following excerpt of the documentation.

Triggers

WebJobs SDK supports the same set of triggers and binding used by Azure Functions. Please note that in the WebJobs SDK, triggers are function-specific and not related to the WebJob deployment type. WebJobs with event-triggered functions created using the SDK should always be published as a continuous WebJob, with Always on enabled.

We first tried to deploy the WebJob with deployment type Triggered as we defined a timer trigger. Because of the above excerpt it didn’t work as expected – the WebJob was not in status Running after deployment and we didn’t manage to change that. A good example of RTFM (Read The Fucking Manual) 😉 However it isn’t very intentional…

Please additionally consider to set Always on setting in the site of the Azure App Service to run continuous WebJobs correctly. Otherwise the runtime goes idle after a few minutes of inactivity. The always on setting is only available in Basic, Standard and Premium pricing tiers!

After considering these things, we finally managed to deploy our WebJob so that it behaved as expected.

To deploy the WebJob together with the .NET Core 6 Web API, we had to add the following configuration to the .NET Core 6 Web API project (.csproj file):

<Project Sdk="Microsoft.NET.Sdk.Web">
  <Target Name="PostpublishScript" AfterTargets="Publish">
    <Exec Command="dotnet publish ../Examples.WebJobs/ -o $(PublishDir)App_Data/Jobs/continuous/ExampleFunction" />
  </Target>

The deployment in the Azure DevOps YAML pipeline is done with task AzureRmWebAppDeployment@4.

Special thanks go to Guillem for his investigation and support!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Website Powered by WordPress.com.

Up ↑

%d bloggers like this: