Deploying an Azure WebJob with GitHub Actions

enter image description here

2022-04 Update: as Lee McMullen has pointed out, the deployment task srijken/azure-zip-deploy@v1.0.1 has been replaced by azure/webapps-deploy@.

WebJobs are a hidden gem within Azure App Service. While it's coupled to the web application, one of the neat tricks is to turn it into a worker service for continuous background processing. Arguably, this could be also accomplished by Azure Functions, but in certain scenarios, it's simpler to have an equivalent of what used to be a Windows Service. I will skip the building a WebJob part. It's sufficiently enough documented by Microsoft. Yves Goeleven has done some really nice work with WebJobs he'd be happy to share with those that are looking for ideas. And while what he's done is great, I will mention that I'd love to see a WebJob as a Service kind of offer coming soon. Or some Worker as a role. Something that would fill in the gap that is there. Not everything is containers/Kubernetes or Functions. And using WebJobs under the App Service umbrella feels like hunting a mosquito with a canon. Without further ado, how to deploy a WebJob using GitHub actions?

Assumptions

Operation system

When using GitHub actions, the operating system used matters. Windows images are twice more expensive than Linux. This directly translates into having half the free minutes when running with Windows. While it's not the biggest factor, there's really no reason to demand a Windows image when building, testing, and deploying to Azure a .NET based WebJob. Therefore, I'll be using a Linux image for this post.

Conditional execution

If you're like me and push almost every commit, it can become quite expensive very fast. I also like to work in PRs, leveraging draft PRs as an indication the work is still in progress. With that in mind, I'd like to avoid unnecessary deployments on PR builds while PRs are marked as draft PRs. In addition to that, I'll skip the packaging of the WebJob if a deployment is not taking place. While it's such a small optimization, it leads to faster build execution.

Managing secrets

GitHub Actions has support for creating and managing secrets. I'll use that and frankly, recommend you always store your secrets that way. In my example, I'll be storing .publishsettings files retrieved from the portal as GitHub secrets.

Project structure

WebJobSolution/
├── .github/workflows/
│   └── dotnet-core.yml
└── src/
    ├── worker
    │   ├── worker.csproj
    │   ├── run.cmd
    │   ├── settings.job
    │   └── ...
    └── worker.sln

Action script

name: .NET Core

env:
  BINARIES: './output/app_data/jobs/continuous/mywebjob' # the last folder name will become the webjob name in the portal
  ZIP_FILE: 'webjob.zip'
  ZIP_PATH: './output'
  ZIP_FILEPATH: './output/webjob.zip'

on:
  push:
    branches: [ master, develop ]
  pull_request:
    branches: [ master, develop ]
    types: [review_requested, ready_for_review]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2

    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.101

    - name: Install dependencies
      run: dotnet restore ./src/worker.sln

    - name: Build
      run: dotnet build ./src/worker.sln --configuration Release --no-restore --output ${{env.BINARIES}}
    
    - name: Test
      run: dotnet test ./src/worker.sln --no-restore --verbosity normal --output ${{env.BINARIES}}

    - name: Zip
      if: github.event_name != 'pull_request' # skip on PRs
      uses: nguyenquyhy/zip-release@v0.3.0
      with:
        filename: '${{env.ZIP_FILE}}'
        workingDirectory:  ${{env.ZIP_PATH}}
        exclusions: 'worker.exe app_data/**/unix/*' # skip .exe and Unix runtime as deployment will be to a Windows App Service

    - name: Deploy to Test (develop branch) # skip on PRs
      if: github.ref == 'refs/heads/develop' && github.event_name != 'pull_request' # skip on PRs, deploy develop branch to the test environment
      uses: srijken/azure-zip-deploy@v1.0.1
      with:
        zip-file: ${{env.ZIP_FILEPATH}}
        publish-profile: ${{ secrets.WEBJOB_PUBLISH_PROFILE_TEST }}

    - name: Deploy to Production (master branch)
      if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' # skip on PRs, deploy master branch to the production environment
      uses: azure/webapps-deploy@v2
      with:
        app-name: ${{ env.AZURE_WEBAPP_NAME }}
        package: ${{env.ZIP_FILEPATH}}
        publish-profile: ${{ secrets.WEBJOB_PUBLISH_PROFILE_PROD }}

With this fairly small script, an automated CI/CD will be executed and deploy the WebJob whenever a change is done on develop or master branches, deploying the bits into the testing or production environment. Happy deploying!

2 Comments

  • Hey Sean,

    First off, thanks for the post, really useful!

    I couldn't quite get it working out of the box and thought I'd add the details here in case anyone else has something similar.

    I was having an issue with the `zip-release` action not being able to find the output directory to pick up the output files. It was throwing this error: `/entrypoint.sh: can't cd to output`. Turns out that was because my sln is structured slightly differently.

    First of all, I didn't want to build the whole solution (there are a couple of other projects in there which I don't need to build along with the webjob) so I'm publishing the actual project (e.g. `dotnet-publish path/to/webjobj -o ${{env.BINARIES}}` instead of restoring and building the whole solution.

    Also my solution is structured slightly different to yours in that my webjob `csproj` is nested a couple of levels below my `.sln`. The `dotnet-publish` command places the output relative to the csproj and so the output ends up a couple of levels deep. This is the cause of my problem! The `zip-release` action is looking for the output directory at the top level based on `env.ZIP_PATH` and naturally can't find it, because it's not actually there :-)

    I managed to work it out after (quite!) a few runs with `ls -al` commands scattered at various levels. The fix was to simply prefix `env.ZIP_PATH` and `env.ZIP_FILEPATH` with the path to the webjob directory and it all started working.

    Thanks again.

    Cheers,
    Lee

  • @Lee, thank you for sharing your scenario and what needed to be adjusted. Glad to hear the post was helpful 🙂

Comments have been disabled for this content.