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: [email protected]
        displayName: Restore Dependencies
        inputs:
          command: 'restore'
          projects: 'MySolution.sln'
      - task: [email protected]
        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: [email protected]
        displayName: Create Zip Package
        inputs:
          rootFolderOrFile: '$(Build.BinariesDirectory)/webjobs'
          includeRootFolder: false
          archiveType: 'zip'
          archiveFile: '$(Build.ArtifactStagingDirectory)/webjobs.zip'
          replaceExistingArchive: true
      - task: [email protected]
        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.