Building Messaging Endpoints in Azure: WebJobs

Posts in this series:

In the last post, I looked at creating a generic host endpoint that many of the deployed versions in Azure can share. By using a hosted service, we can then host NServiceBus in just about anything that can work with the .NET Core generic host. The differences then come to hosting and scaling models.

First up is the closest we have to "Platform-as-a-Service" for background tasks - Azure WebJobs. WebJobs can be any executable/script, but a very common model for building is to use the Azure WebJobs SDK.

Azure WebJobs are a fairly robust implementation of a hosted service - all of the triggers and execution models just piggyback on top of an IHostedService implementation. Instead of configuring individual executables with separate trigger models, we can host multiple "jobs" inside a single deployed host.

So what does this mean for our humble generic host we created earlier?

Picking a WebJobs model

In the last post where we created our own IHostedService instance, this would be separate from the WebJobs SDK "host". So we have a couple of options:

  • Use our generic host inside a stock WebJob
  • Use a trigger inside a WebJobs SDK host

With the first option, we can also technically host the WebJobs SDK host, since multiple hosts are supported, so really the question becomes, "will we have other WebJobs to execute or not?"

With the WebJobs SDK, we can host any kind of triggered job, from "cron"-style jobs, to message-driven, continuous and more.

It's really dependent on the other things we have going on outside of our generic host. If we wanted to go strictly with Azure Web Jobs SDK, we'd have to create a "continuous" trigger and ditch our generic host. There's really not much benefit to that choice that I've found, so the path of least resistance is to simply host our generic host inside an executable deployed as-is.

Configuring our WebJobs Project

The WebJobs project is really just a console application project. It doesn't really matter up front, but we can choose to output as an assembly or EXE. Either way, the piece we need to connect just a plain console application to WebJobs is some way to run the WebJob. For this, we create a file called "run.cmd" with instructions on how to run the WebJob:

dotnet WebJobReceiver.dll

If we go pure EXE, we'd just have the EXE file name in our run.cmd file. Finally, we just need to make sure when we build/deploy our application, this file is included in the final published version. We can do this in our .csproj file:

<ItemGroup>
  <Content Include="run.cmd">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>

With this in place, we can dotnet publish our project and the we can deploy this out to Azure. But where should this get deployed?

Deploying Azure WebJobs

Azure WebJobs can't be deployed just by themselves - they have to deployed as part of an Azure AppService (and not a Linux AppService, either). This is somewhat annoying - anything we push out has to be tied with an AppService, for both build and deployment. You can technically push out a WebJob independent of an AppService deployment - but it's a bit ugly.

Azure WebJobs also have a few other disadvantages:

  • They share the host AppService's resources
  • They cannot scale independently of the host AppService

In short, if we don't expect too many messages, a WebJob will be fine. We can also technically deploy a WebJob alongside a "null" parent AppService. The parent AppService can be blank/nothing. But again, it's a bit weird.

Deploying a WebJob means we need to combine the packages of the parent AppService and child WebJobs, which is fairly straightforward to do in an Azure DevOps build pipeline. We first have a step to publish the AppService:

steps:
- task: DotNetCoreCLI@2
  displayName: 'Publish Sender'
  inputs:
    command: publish
    arguments: '-c Release --no-build --no-restore -o $(Build.ArtifactStagingDirectory)'
    zipAfterPublish: false
    workingDirectory: Sender

Then one to publish the WebJob:

steps:
- task: DotNetCoreCLI@2
  displayName: 'Publish WebJobReceiver'
  inputs:
    command: publish
    publishWebProjects: false
    projects: WebJobReceiver
    arguments: '-c Release --no-build --no-restore -o $(Build.ArtifactStagingDirectory)\Sender\app_data\jobs\continuous\WebJobReceiver'
    zipAfterPublish: false
    modifyOutputPath: false

Very important here is the output folder for our project - we have to publish our WebJob into a very specific folder in the parent AppService's published output. Finally, we publish the App Service, which includes the WebJob:

steps:
- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: Sender'
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)\Sender'
    ArtifactName: Sender

With this in place, we can deploy this artifact to an Azure App Service, and see our WebJob in the Azure portal:

Azure WebJob inside Azure Portal

And our message endpoint is deployed!

Inevitably the question comes up here - why not a ServiceBus trigger? Why go through all this? We'll get to this in a later post when we look at Azure Functions, but next up, Azure Container Instances.