Building Messaging Endpoints in Azure: WebJobs
Posts in this series:
- Evaluating the Landscape
- A Generic Host
- Azure WebJobs
- Azure Container Instances
- Azure Functions
- Azure Container Apps
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:
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.