WebJobs auf Azure deployen
WebJobs bieten die Möglichkeit, Programme oder Skripts innerhalb einer ASP.NET Webapp auszuführen. Sie eignen sich sehr gut, um wiederkehrende Hintergrund-Tasks auszuführen. Mit dem Azure WebJobs SDK ist es möglich, WebJobs als Teil einer normalen Konsolenanwendung zu entwickeln. Ich habe kürzlich versucht, WebJobs als Teil einer Konsolenanwendung in einen Azure App Service zu deployen und hatte einige Probleme, dies zum Laufen zu bekommen. Deshalb zeige ich dir in diesem Beitrag wie es funktioniert.
WebJobs in einer Konsolenanwendung
Auch wenn es für das Deployment irrelevant ist, möchte ich dir kurz zeigen, wie du WebJobs als Teil einer normalen Konsolenanwendung zum Laufen bekommst. Dazu erstellen wir in Visual Studio eine ganz normale Konsolenanwendung. Danach editieren wir den Startup Code in Program.cs
wie folgt:
class Program
{
static async Task Main()
{
var hostBuilder = new HostBuilder();
hostBuilder.ConfigureWebJobs(webJobsBuilder =>
{
webJobsBuilder.AddAzureStorageCoreServices();
webJobsBuilder.AddTimers();
// weitere Trigger registrieren
});
var host = hostBuilder.Build();
using (host)
{
await host.RunAsync();
}
}
}
Damit das Ganze funktioniert müssen wir noch die Abhängigkeiten Microsoft.Azure.WebJobs
und Microsoft.Azure.WebJobs.Extensions
hinzufügen. Ich habe in diesem Beispiel Timer Trigger mittels AddTimers()
registriert, damit ich meine WebJobs mit Cron Expressions triggern kann. Falls du andere Trigger benötigst, kannst du diese ganz einfach ergänzen.
Damit wir beim Starten der Konsolenanwendung auch etwas sehen, müssen wir noch zusätzlich das Logging in der Konsole konfigurieren. Dazu müssen wir die Abhängigkeit Microsoft.Extensions.Logging.Console
installieren und in unserem Startup Code die Konfiguration vornehmen:
class Program
{
static async Task Main()
{
var hostBuilder = new HostBuilder();
hostBuilder.ConfigureLogging((context, loggingBuilder) =>
{
loggingBuilder.AddConsole();
});
hostBuilder.ConfigureWebJobs(webJobsBuilder =>
{
webJobsBuilder.AddAzureStorageCoreServices();
webJobsBuilder.AddTimers();
// weitere Trigger registrieren
});
var host = hostBuilder.Build();
using (host)
{
await host.RunAsync();
}
}
}
Das ist grundsätzlich alles, was wir tun müssen um WebJobs in einer Konsolenanwendung zu verwenden. Ich habe meinen Startup Code noch wie folgt erweitert, damit ich meine eigenen Services registrieren kann und damit die Konfiguration so geladen wird, wie ich es gerne hätte:
class Program
{
static async Task Main()
{
var hostBuilder = new HostBuilder();
hostBuilder.ConfigureServices((context, services) =>
{
// eigene Services registrieren
});
hostBuilder.ConfigureLogging((context, loggingBuilder) =>
{
loggingBuilder.AddConsole();
});
hostBuilder.ConfigureWebJobs(webJobsBuilder =>
{
webJobsBuilder.AddAzureStorageCoreServices();
webJobsBuilder.AddTimers();
// weitere Trigger registrieren
});
hostBuilder.ConfigureAppConfiguration(configurationBuilder =>
{
configurationBuilder.Sources.Clear();
configurationBuilder
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
#if DEBUG
.AddJsonFile("appsettings.development.json", optional: true, reloadOnChange: true)
#endif
.AddEnvironmentVariables();
});
var host = hostBuilder.Build();
using (host)
{
await host.RunAsync();
}
}
}
Eine CI/CD Pipeline für die WebJobs
Um die WebJobs auf Azure zu deployen, erstellen wir eine CI/CD Pipeline auf Azure DevOps. In einem früheren Beitrag habe ich dir gezeigt, wie du mit GitHub Actions eine Static Site deployen kannst. Die Syntax für eine Azure DevOps Pipeline ist beinahe identisch, die Unterschiede liegen in den Details. Hier zunächst einmal die komplette Pipeline als YAML:
trigger:
- main
jobs:
- job: build_and_deploy
pool:
vmImage: windows-latest
steps:
- task: DotNetCoreCLI@2
displayName: Restore Dependencies
inputs:
command: 'restore'
projects: 'MySolution.sln'
- task: DotNetCoreCLI@2
displayName: Build WebJobs
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/MyWebJobs.csproj'
arguments: '--output $(Build.BinariesDirectory)/webjobs/App_Data/jobs/continuous'
zipAfterPublish: false
modifyOutputPath: false
- task: ArchiveFiles@2
displayName: Create Zip Package
inputs:
rootFolderOrFile: '$(Build.BinariesDirectory)/webjobs'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/webjobs.zip'
replaceExistingArchive: true
- task: AzureRmWebAppDeployment@4
displayName: Deploy WebJobs
inputs:
ConnectionType: 'AzureRM'
azureSubscription: 'MyAzureSubscription'
appType: 'webApp'
WebAppName: 'my-webapp'
packageForLinux: '$(Build.ArtifactStagingDirectory)/webjobs.zip'
Den richtigen Pfad verwenden
Damit Azure bzw. der Azure App Service die WebJobs erkennt, müssen diese im richtigen Verzeichnis abgelegt werden. Azure erwartet, dass die WebJobs entweder im Verzeichnis App_Data/jobs/continuous
oder App_Data/jobs/triggered
der WebApp sind. Dabei spielt es keine Rolle, ob die WebJobs zusammen mit einer WebApp oder in unserem Fall als eigenständige Konsolenanwendung deployed werden. WebJobs im Verzeichnis continuous
werden automatisch gestartet und laufen immer. WebJobs im Verzeichnis triggered
müssen manuell angestossen werden.
Damit die WebJobs im korrekten Verzeichnis landen, geben wir beim Erstellen mit dem dotnet publish
Befehl das entsprechende Verzeichnis an und erstellen anschliessend manuell eine Zip Datei, die die korrekte Verzeichnisstruktur enthält.
Erstellen unter Windows
WebJobs funktionieren nur in Azure App Services die mit Windows laufen. Mit der CI/CD Pipeline sollte dies eigentlich nichts zu tun haben. Ich lasse normalerweise alle meine Pipelines wenn möglich unter Linux laufen, weil die meisten Schritte unter Linux erfahrungsgemäss schneller durchlaufen. Da die von mir erstellte Konsolenanwendung auf .NET 5 basiert, währe das hier auch möglich. Damit der dotnet publish
Befehl jedoch eine Exe Datei aus unseren WebJobs erstellt, die vom Azure App Service korrekt erkannt wird, müssen wir die Pipeline unter Windows laufen lassen. Dies erreichen wir mittels der Verwendung von vmImage: windows-latest
.