Originally published for Afterman Software

Posts in this Series:

In Part 2, we used Visual Studio's Publish feature to deploy our NServiceBus endpoint to an Azure App Service, and after only one manual change in the Azure Portal, saw our endpoint successfully running by viewing the app service logs.

As easy and convenient as Visual Studio Publish is to use, there are two problems with it.

First, using Visual Studio publish feels a bit like this

PayNoAttenstionToTheManBehindTheCurtain

There is a lot going on "behind the curtain" that we're taking for granted which is allowing us to move very quickly (which is good), but with little to no knowledge of what's actually going on (which is bad).

Second, to graduate beyond demo-ware deployment, we need to understand what Visual Studio Publish is actually doing. Why? We'll need that knowledge to implement a more robust deployment solution like Azure DevOps because in most real-world projects

  • There are more than two environments (your local machine and production)
  • You don't deploy directly to production from your local machine

In this post, we'll be pulling back the curtain on Visual Studio Publish to examine what's happening in your local solution and in Azure.

Getting the Code

We won't be making any code changes in this post in the series, so I won't be providing a Part 3 branch on GitHub repo. Also, if you haven't completed the work in Part 2, where Visual Studio Publish provisions Azure resources, you won't be able to follow along with the end of this post.

If you've already completed Part 2, you can feel free to grab the code for Part 2 here.

Project Artifacts

Visual Studio publish added two artifacts to the project. Settings.job and a PublishProfiles folder with two files in it.

VisualStudioArtifactsAdded-1

Settings.job

The Settings.job file has examples of CRON expressions that can be used for Triggered WebJobs.

{
  //    Examples:
  //    Runs every minute
  //    "schedule": "0 * * * * *"
  ...
}

Since our WebJob is Continuous, this file won't be used and you can ignore it.

Publish Profiles

The PublishProfiles folder contains two files, .pubxml and .pubxml.user. Let's look at NSBEndpointInWebJob - Web Deploy.pubxml first.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>MSDeploy</WebPublishMethod>
    <PublishProvider>AzureWebSite</PublishProvider>
    <LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish>https://nsbendpointinwebjob.azurewebsites.net</SiteUrlToLaunchAfterPublish>
    <LaunchSiteAfterPublish>False</LaunchSiteAfterPublish>
    <ResourceId>/subscriptions/[AZURE_SUBSCRIPTION_IDENTIFIER]/resourcegroups/NSBEndpointInWebJob/providers/Microsoft.Web/sites/NSBEndpointInWebJob</ResourceId>
    <UserName>$NSBEndpointInWebJob</UserName>
    <_SavePWD>True</_SavePWD>
    <WebJobType>Continuous</WebJobType>
    <WebJobName>NSBEndpointInWebJob</WebJobName>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <MSDeployServiceURL>nsbendpointinwebjob.scm.azurewebsites.net:443</MSDeployServiceURL>
    <MSDeployPublishMethod>WMSVC</MSDeployPublishMethod>
    <SkipExtraFilesOnServer>True</SkipExtraFilesOnServer>
    <EnableMsDeployAppOffline>False</EnableMsDeployAppOffline>
    <EnableMSDeployBackup>True</EnableMSDeployBackup>
    <DeployIisAppPath>NSBEndpointInWebJob</DeployIisAppPath>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <SelfContained>false</SelfContained>
  </PropertyGroup>
</Project>

In this file, you'll see some of the settings we supplied when we initially published the project like <WebJobType> set to Continuous. The file also tells us Visual Studio is using MSDeploy (aka, Web Deploy) to deploy our project to Azure.

The <_SavePWD> setting is set to true, but where is it saved? In the NSBEndpointInWebJob - Web Deploy.pubxml.user file.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <EncryptedPassword>[ENCRYPTED_PASSWORD]</EncryptedPassword>
  </PropertyGroup>
</Project>

The .pubxml file can be modified to customize the build and publish process after it's been created by Visual Studio. You can edit the file directly or still use the Publish editor in Visual Studio to change values in the file. You can also create multiple publishing profiles (for instance, one for each of your environments).

Also, you don't need Visual Studio to deploy your project to Azure. You can do that from the command line and pass it the publish profile.

Publish Profiles simplify the publishing and deployment process and can be used by different build and deployment models supported by .NET Core.

Because both publish files contain sensitive information, neither should be checked into source control.

Build Output

In Part 2, when we clicked the "Publish" button there was a lot of output generated in the Build Output window in Visual Studio. Some of the secrets to publishing a WebJob to an app service are in this output. The output is quite lengthy, so I'll be focusing on the most important parts of it that will show us what the Publish process is doing behind the scenes.

The "Magic Path" to WebJobs

This is the first hint that WebJobs cannot simply be deployed to the root path of the app service (like a web app can)

NSBEndpointInWebJob -> D:\Projects\NSBEndpointInWebJob\NSBEndpointInWebJob\obj\Release\netcoreapp3.1\PubTmp\Out\app_data\Jobs\Continuous\NSBEndpointInWebJob\

More specifically, this sub-section from the path above is the magic path to where WebJobs need to live in the app service

\app_data\Jobs\Continuous\NSBEndpointInWebJob\

If our WebJob was Triggered, the path would contain \Triggered instead of \Continous.

Next, the publish process adds each directory until it builds the full WebJob path

Adding directory (NSBEndpointInWebJob\app_data).
Adding directory (NSBEndpointInWebJob\app_data\Jobs).
Adding directory (NSBEndpointInWebJob\app_data\Jobs\Continuous).
Adding directory (NSBEndpointInWebJob\app_data\Jobs\Continuous\NSBEndpointInWebJob).

Then, each file required for deployment is added to that path

Adding file (NSBEndpointInWebJob\app_data\Jobs\Continuous\NSBEndpointInWebJob\appsettings.json).
...

Files that are usually deployed to the app service's root folder are instead deployed to the WebJob path. The path is a convention where WebJobs must live to be recognized by the app service so they can be discovered and then run.

But what actually invokes the WebJob? The answer is in this line of output

Adding file (NSBEndpointInWebJob\app_data\Jobs\Continuous\NSBEndpointInWebJob\run.cmd).

What is run.cmd?

No, not these guys

RunDmc2

Run.cmd is a file that is auto-generated by Visual Studio Publish. It's a directive to the app service to start a command prompt and run something. But what is it running? It's running the binary output of our project which is the WebJob.

WebJobs use the kudu engine to run, and the kudu engine has conventions for the types of files it will scan for that are runnable scripts.

Without this file, the WebJob will have no way to be discovered or run. Visual Studio Publish is nice enough to generate it for us instead of relying on us to add a run.cmd file in our project.

Azure Artifacts

At the end of Part 2, we shut down our Azure resources so we would not get any nasty billing surprises. We used the Azure CLI in Cloud Shell to make that happen. In order to explore the Azure artifacts that were created when we deployed in Part 2, we need to start those resources again.

Restoring Azure Resources

Open Azure Portal and click on the Cloud Shell icon to start your PowerShell session

ClickOnCloudShell

CloudShellReady

Once your Cloud Shell session is running, we'll restart the Azure resources

Change App Service Plan to B1

Before we turn our app service back on and set it to Always On, we first need to change the service plan to the B1 pricing tier.

Copy and paste the following PowerShell code into CloudShell and hit Enter

az appservice plan update -n "NSBEndpointInWebJob" -g "NSBEndpointInWebJob" --sku B1 --only-show-errors

Start App Service

Now that our service plan is running in a pricing tier that will allow us to run the app service in Always On, we can set the app service to Always On. Copy and paste the following PowerShell code into Cloud Shell replacing NAME_OF_YOUR_APP_SERVICE with your unique app service name.

az webapp config set -g "NSBEndpointInWebJob" -n "NAME_OF_YOUR_APP_SERVICE" --always-on true

and then start the app service

az webapp start -n "NAME_OF_YOUR_APP_SERVICE" -g "NSBEndpointInWebJob"

With our Azure resources up and running, you should be able to go the app service's WebJobs menu item in the Azure Portal, click on the WebJob menu item and then view the Logs to make sure the endpoint is running (you can refer to Part 2 of this series for the specifics).

Exploring Azure Artifacts

Besides the Azure resources that were provisioned by Visual Studio Publish (storage account, app service plan and app service) what was actually deployed to the app service? Let's take a look.

Open the Azure Portal and go to your app service. Type console in the search field and then click on the "Console" menu item

SearchForAndClickOnConsoleInAppService

You'll see the app service Console open to the root directory of the app service

AzureAppServiceConsole

Type dir and you'll see the first folder in the WebJob path, app_data

AppServiceConsoleTypeDirInRootDirAndSeeAppData

Type in the following command and you'll be brought to the root directory of your WebJob

cd app_data\Jobs\Continuous\NSBEndpointInWebJob\

Then type dir and you should see a list of all deployed artifacts.

AppServiceConsoleDirListInWebJobPath

Scrolling through the list, you should see appsettings.json, Settings.job, run.cmd and the rest of the .dll's that were deployed with your project

Let's take a look at the contents of run.cmd. Type more run.cmd. You should see the following output

RunCmdContents

The contents of the file uses the dotnet command to run the compiled binary that is your WebJob, which hosts the NServiceBus endpoint.

Cleaning Up

Just like like last post, we want to shut down and clean up our Azure resources. But we're going to add a new step, which is deleting all the files and folders under the app_data directory in the web root to give us a "clean slate" to deploy our NServiceBus endpoint using Azure DevOps in Part 4.

Return to the Console of the app service and make sure the path is D:\home\site\wwwroot when you open it. Type the following command

rmdir /Q /S app_data

This command will delete the app_data folder and all folders and files in it. Basically, we're deleting our NServiceBus endpoint.

Next, let's shut down and clean up our Azure resources. Enter the following Azure CLI commands one after another in Cloud Shell replacing NAME_OF_YOUR_APP_SERVICE and NAME_OF_YOUR_STORAGE_ACCOUNT with your app service and storage account name.

az webapp config set -g "NSBEndpointInWebJob" -n "NAME_OF_YOUR_APP_SERVICE" --always-on false
az webapp stop -n "NAME_OF_YOUR_APP_SERVICE" -g "NSBEndpointInWebJob"
az appservice plan update -n "NSBEndpointInWebJob" -g "NSBEndpointInWebJob" --sku FREE --only-show-errors
az storage message clear -q "NAME_OF_YOUR_STORAGE_ACCOUNT-audit" --account-name "NAME_OF_YOUR_STORAGE_ACCOUNT" --only-show-errors

In Closing

In this post, we did a deep dive into the mechanics behind Visual Studio Publish. We learned about Publish Profiles, Web Deploy and the specific artifacts and conventions required in order to deploy a WebJob successfully to an Azure app service. We re-started our Azure resources and used the app service console to take a look at the generated folders and files created by Visual Studio Publish to host our NServiceBus endpoint.

In Part 4, the final part of this series, we'll be putting our newfound knowledge into action as we create a build and deployment pipeline for our project using Azure DevOps.

References