Quote of the Day

more Quotes

Categories

Get notified of new posts

Buy me coffee

  • Home>
  • .NET core>

Build and deploy a WebJob alongside web app using azure pipelines

Published December 13, 2021 in .NET , .NET core , ASP.NET core , Azure , Devops - 1 Comment

In this post, I’m going to share some of the issues, misunderstandings I ran into when trying to setup and deploy a WebJob alongside a web application using azure pipelines. The WebJob is a console application, and the web app is an ASP.NET core. Both the WebJob and web app target .NET 5.

Microsoft’s documentation on azure WebJob provides good info on how to create and publish a WebJob. However, . In case you are not familiar with WebJobs, below is a brief description from the document:

WebJob is a feature of Azure App Service that enables you to run a program or script in the same instance as a web app, API app, or mobile app. There is no additional cost to use WebJobs.

Run background tasks with WebJobs in Azure App Service

You can create two types of WebJobs, Continuous or Triggered. As the names suggest, a continuous WebJob always run, whereas a triggered WebJob runs on demand or a schedule. In my case, I setup a triggered WebJob to send reports via email on a schedule.

Let’s go over a few things I misunderstood or did not realize about WebJob.

You don’t need to target .NET framework to deploy a WebJob together with a web app

When going over the Microsoft’s documentation on WebJobs, I saw a section on deploying WebJobs using Visual Studio. The section mentions linking a WebJob with a web project such that when you deploy the web project, the web job is already a part of it.

Deploy a project as a WebJob by itself, or link it to a web project so that it automatically deploys whenever you deploy the web project.

WebJobs as .NET Framework console apps

If you want to link a WebJob with a web project, the web job needs to target .NET framework, not .NET core.

.NET Core Web Apps and/or .NET Core WebJobs can’t be linked with web projects. If you need to deploy your WebJob with a web app, create your WebJobs as a .NET Framework console app.

WebJobs as .NET Core console apps

I was confused at first when reading this section. Specially, I was not sure if I need to target .NET framework when building the WebJob if I want to deploy it together with the web app. The answer is No. Note that this section is only for deploying the WebJob using Visual Studios. In other words, the WebJob only needs to target .NET framework if you want to deploy both the WebJob and web app together using Visual Studios. In my case, since I use azure devops, I was able to deploy my WebJob and web app together, both targeting .NET 5.

The WebJob needs to have a supported executable file

When trying to deploy the WebJob, I was wondering why the WebJob did not show up in the azure portal when using azure devops to automatically deploy both the WebJob and the web application. However, if I used visual studios to deploy the WebJob, then it showed up correctly. I found online posts such as this StackOverflow and this article and got the wrong impression that the issue had to do with running from package. When using azure app service deployment task, azure devops automatically updates the app service’s config and turn on the WEBSITE_RUN_FROM_PACKAGE flag. I spent a lot of time trying to modify the pipeline to not set the flag, or removed the flag myself, but could not get it to work.

It turned out the issue was that I was using an ubuntu agent to publish the WebJob, which only produced .dll files by default. My WebJob is a .NET 5 console app, and I had to specify the target platform (win-x64) to produce the .EXE file, as shown in the snippet below. When I was using Visual Studios to publish the WebJob, I was using a windows machine, and the result artifact contained the .exe file, which is expected for the WebJob to show up.

- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: false
    projects: '**/WebJob.csproj'
    arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)/App_Data/jobs/triggered -r win-x64 --self-contained false'
    zipAfterPublish: false
  displayName: 'Publish web job'

The WebJob can’t call the web app via HTTP if both are running on a same instance

My initial plan was to build all the functionalities in the web app and expose them via REST endpoints. However, because both the WebJob and web app run on a same app service, it’s not possible to make HTTP calls from the WebJob to the web app without workarounds. Per the document,

Connection attempts to local addresses (e.g. localhost127.0.0.1) and the machine’s own IP will fail, except if another process in the same sandbox has created a listening socket on the destination port. The listening port must be > 1024 and not currently in used.

Azure WebApp sandbox

Below is the exception I got, as stated in the document

Exception Details: System.Net.Sockets.SocketException: An attempt was made to access a socket in a way forbidden by its access permissions 127.0.0.1:...

Missing the AzureWebJobsDashboard configuration warning in the Azure WebJob Dashboard

If you don’t set the AzureWebJobsDashboard configuration in the azure app service, you may see some warnings in the azure WebJob dashboard, as shown below.

Missing AzureWebJobsDashboard configuration

However, this is just a warning and does not cause any harm. As per this github issue, it appears as if this config is necessary only for dashboard logging using azure blob storage. In my case, I was able to log to AppInsights, so I did not have to set the config. However, I also noticed that the dashboard only displays up to a certain number of logs. So, you may want to set the config if you want to persist logs using azure blob storage, or setup AppInsights logging.

The fundamental problem that you’re running into is that the webjobs dashboard UX cannot be be used for webjobs 3.x onwards and functions 2.x onwards. It has been replaced with App Insights integration. We are no longer making any engineering investments into this UX.

Make sure that you are setting a connection string named AzureWebJobsDashboard 

If triggered WebJob does not run at the scheduled time, check the timezone.

I setup my WebJob to run on a schedule, by specifying the cron expression in the Settings.job file. At first, I thought it did not run at the right time that I expected based on the cron expression. I realized the issue is because the server’s time was in UTC. I got the WebJob to run correctly at the setup scheduled by adding the WEBSITE_TIME_ZONE configuration to the app service. See this link for for more info.

Now that I have gone over some of the misunderstandings I had about WebJob, in the next section, I’ll quickly go over the project and pipeline setup.

WebJob project

The WebJob is basically a .NET 5 console application. I use the host builder and configure dependency injection, load configurations from json files, environment variables, key vault, configure AppInsights logging etc … For instructions on how to develop the WebJob using WebJobs SDK, checkout the document. One caveat is that I could not get the correct environment name from the HostingEnvironment of the HostBuilderContext. I used the Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") to retrieve the environment.

// codes omitted for brevity 
using Microsoft.Azure.WebJobs;

namespace WebJob
{
    public class Program
    {
        private static IConfigurationRoot _configuration;

        static async Task Main(string[] args)
        {
            var builder = new HostBuilder();

            builder.ConfigureWebJobs(b =>
            {

            });
            builder.ConfigureAppConfiguration((context, config) =>
            {
                ConfigureAppConfiguration(config, context);
            });

            builder.ConfigureLogging((context, loggingBuilder) =>
            {

                loggingBuilder.AddConsole();
                string appInsightsInstrumentationKey = _configuration["APPINSIGHTS_INSTRUMENTATIONKEY"];
                if (!string.IsNullOrEmpty(appInsightsInstrumentationKey))
                {
                    Console.WriteLine("Configuring applicationInsights logging.");
                    loggingBuilder.AddApplicationInsightsWebJobs(o => {
                        o.InstrumentationKey = appInsightsInstrumentationKey;         
                        });
                }
            });

            builder.ConfigureServices(services =>
            {
                ConfigureServices(services);
            });


            var host = builder.Build();
            using (host)
            {
                var jobHost = host.Services.GetService(typeof(IJobHost)) as JobHost;
                // codes omitted for brevity 

                var paymentReportJob = new PaymentReportJob(...);
                var inputs = new Dictionary<string, object>
                {
                    { "reportJob", paymentReportJob }
                };

                await host.StartAsync();
                await jobHost.CallAsync("DoReport", inputs);
                await host.StopAsync();
            }
        }

        [NoAutomaticTrigger]
        public static async Task DoReport(PaymentReportJob reportJob)
        {
            await reportJob.SendReportAsync();
        }

        private static void ConfigureServices(IServiceCollection services)
        {
            services.Configure<SmtpConfigOptions>(_configuration.GetSection("SmtpConfigOptions"));
            // codes omitted for brevity 
        }

        private static void ConfigureAppConfiguration(IConfigurationBuilder config, HostBuilderContext context)
        {
            var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            Console.WriteLine("Environment: " + environment);
            config.AddJsonFile("appsettings.WebJob.json", optional: false, reloadOnChange: true);
            // codes omitted for brevity 
            _configuration = config.Build();
        }
    }

   // codes omitted for brevity 
}

Build pipeline

Below snippets contain just the relevant steps for building the WebJob and package it together with the web application in a single artifact, ready for deploying to the app service.

trigger:
- "master"

pool:
  vmImage: 'ubuntu-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  
  buildConfiguration: 'Release'

steps:
- checkout: self

# some codes omitted for brevity 


- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: true
    modifyOutputPath: false
    arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)'
    zipAfterPublish: false
  displayName: 'Publish web project'

- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: false
    projects: '**/WebJob.csproj'
    arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)/App_Data/jobs/triggered -r win-x64 --self-contained false'
    zipAfterPublish: false
  displayName: 'Publish web job'

- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: '$(Build.ArtifactStagingDirectory)'
    includeRootFolder: false
    archiveType: 'zip'
    archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'

- task: PublishPipelineArtifact@1
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
  displayName: 'Publish artifact'

In the above code snippets, notice that:

  • I have two DotNetCoreCLI@2 tasks, one for publishing the web app and one for publishing the WebJob.
  • In the ‘Publish web job’ task, I pass as parameters the runtime (-r) with value win-x64 and self-contained flag set to false.
    • Without setting the runtime, the result artifact files do not contain the executable (.exe) file. This is because I’m using an ubuntu agent for the build.
    • Without turning off self-contained flag, I got error because the WebJob which is an executable references the other projects which are non-executable and non-self-contained. See this document for more details.
  • The WebJob and the web app artifact files go under a same root folder: $(Build.ArtifactStagingDirectory).
  • The WebJob files go to the subdirectory: $(Build.ArtifactStagingDirectory)/App_Data/jobs/triggered.
    • On azure app service, azure expects a triggered WebJob to be under the subdirectory App_Data/jobs/triggered relative to the root directory of the artifact. If your WebJob is continuous, then you need to make sure the codes go under /site/wwwroot/app_data/Jobs/Continuous, as per the document.
  • I used the ArchiveFiles@2 task to create the result zip file, ready for deployment.

Release pipeline

I did not have to do anything special in the release pipeline. This is because the WebJob is just part of the web app, and the build pipeline put everything together in a single zip file.

References

Run background tasks with WebJobs in Azure App Service

Error generated when executable project references mismatched executable

Making requests to localhost within Azure App Services application

How do I set the server time zone for my web app?

Make sure that you are setting a connection string named AzureWebJobsDashboard 

1 comment