Bot Framework v4 – Continuous Deployment: Part 2 – ARM template and Azure DevOps Release Pipeline

This post is the second part of a two-part blog post. In the first blog post we mainly focused on laying the groundwork for the release of the Bot. We made some adjustments to an existing Bot Project from the Bot Builder Samples repository and created a build pipeline in Azure DevOps.

In this post we will focus on deploying the Bot to Azure. We will create an ARM template to declaratively describe the resources we need to provision on Azure and we will create the actual release pipeline in Azure DevOps.

Creating the ARM template

The build pipeline we created in the first part of this blog post produced an artifact which contains our build Bot project in the form of a zip file. We can use this artifact to deploy the Bot to Azure, but at the moment we don’t have anything to deploy to yet. Let’s do a quick inventory of the required resources.

When we create a Web App Bot from the Azure portal, Azure will create a Bot Service and an App Service Web App. The App Service will host and run our Bot and it will provide an endpoint to be used by the Bot Service to connect to the Bot. In the Bot Service we can configure (channels, authentication, analytics), test and scale our Bot. Every Web App requires an App Service Plan and we also use LUIS and Application Insights as external services in our Bot. That means that for this project we need to create the following services in Azure:

  • App Service Plan
  • Web App
  • Bot Service
  • Application Insights
  • Language Understanding (LUIS)

We will use anARM (Azure Resource Manager) template to create all of these resources in Azure. An ARM template is a JSON file that describes one or more Azure resources and can be used in an Azure DevOps Release pipeline to automatically provision them. For this blog post I will assume you have some basic knowledge of ARM templates. If that’s not the case you can read more on ARM templates and look at some examples here.

We could add the template files to the existing Bot project, but I prefer to keep them in a separate project. Add a new project to the solution. Select cloud in the left menu and Azure Resource Group as the project template.

Create a Resource Group Project

Create a Resource Group Project

When creating the project, Visual Studio will give us some basic templates to choose from. Select the Blank template because this will give us a nice clean template to start building on.

Select blank ARM tempate

Select blank ARM tempate

The created project will have two JSON files, azuredeploy.json and azuredeploy.parameters.json.

Created Template and Parameters file

Created Template and Parameters file

Azuredeploy.json is the ARM template file, the other file contains values for parameters that are used in the template file. Some settings of a resource (f.e. the name or scale unit) can differ for the various environments you want to deploy to. We can use different parameter files for each of those environments, but still use the same template file. For the purpose of simplicity we will not use the parameter file in this blog post but provide default values for most parameters. We will not give a default value to the parameters that contain secrets. Those values will be provided at release time with the help of (secret) pipeline variables. More on that later.

The azuredeploy.json file contains the basic JSON structure for an ARM template. with parameters, variables, resources and outputs:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {},
  "variables": {},
  "resources": [],
  "outputs": {}
}

Now it’s time to start adding the different resources to the template. The Web App, App Service Plan and Application Insights resources are pretty common and I will not go in to much detail on how to add them. I will just show you the parameters and resources to add to the ARM template. In case you don’t want to follow along and just get the completed ARM template, you can get it in this gist.

Add App Service Plan

Add the following parameters to the parameters section:

"hostingPlanName": {
  "type": "string",
  "defaultValue": "bvubotv4hostingplan",
  "minLength": 1
},
"skuName": {
  "type": "string",
  "defaultValue": "F1",
  "allowedValues": [
    "F1",
    "D1",
    "B1",
    "B2",
    "B3",
    "S1",
    "S2",
    "S3",
    "P1",
    "P2",
    "P3",
    "P4"
  ]
},
"skuCapacity": {
  "type": "int",
  "defaultValue": 1,
  "minValue": 1
}

And to the resources section

{
  "apiVersion": "2015-08-01",
  "name": "[parameters('hostingPlanName')]",
  "type": "Microsoft.Web/serverfarms",
  "location": "[resourceGroup().location]",
  "tags": {
    "displayName": "HostingPlan"
  },
  "sku": {
    "name": "[parameters('skuName')]",
    "capacity": "[parameters('skuCapacity')]"
  },
  "properties": {
    "name": "[parameters('hostingPlanName')]"
  }
}

Notice how we use the parameters when defining the resource.

Add Web App

Add to parameters:

"webAppName": {
  "type": "string",
  "defaultValue": "bvubotv4"
},
"botFilePath": {
  "type": "string"
},
"botFileSecret": {
  "type": "string""
}

Add to resources:

{
  "apiVersion": "2015-08-01",
  "name": "[parameters('webAppName')]",
  "type": "Microsoft.Web/sites",
  "location": "[resourceGroup().location]",
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
  ],
  "properties": {
    "name": "[parameters('webAppName')]",
    "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]",
    "siteConfig": {
      "appSettings": [        
        {
          "name": "botFilePath",
          "value": "[parameters('botFilePath')]"
        },
        {
          "name": "botFileSecret",
          "value": "[parameters('botFileSecret')]"
        },
        {
          "name": "WEBSITE_NODE_DEFAULT_VERSION",
          "value": "8.9.4"
        }
      ]
    }
  }
}

Remember that appsettings.json (or secrets.json) had a botFileSecret and botFilePath property? It’s best practice to add properties from the local appsettings.json file to the appSettings section in the ARM resource. These environment variables will override the local variables (appsettings.json). This way we don’t have to redeploy the Bot when a setting has to change but we can simply go the Azure portal and update the values there.

We also define a default NodeJS version, this will ensure NodeJS is available in the App Service. We will need it to install the MSBot CLI and execute commands.

Add Application Insights

Add to parameters:

"appInsightsName": {
  "type": "string",
  "defaultValue": "bvubotv4appinsights"
}

Add to resources:

{
  "apiVersion": "2014-04-01",
  "name": "[parameters('appInsightsName')]",
  "type": "Microsoft.Insights/components",
  "kind": "web",
  "location": "[resourceGroup().location]",
  "dependsOn": [
    "[resourceId('Microsoft.Web/sites/', parameters('webAppName'))]"
  ],
  "properties": {
    "applicationId": "[parameters('webAppName')]"
  }
}

Add Bot Service

All the resources described so far are pretty common with plenty of documentation and examples online. The Bot Service is different though. I couldn’t find any examples online and even the GitHub repository containing all the ARM schemas didn’t contain a schema for the Bot Service. So I tried a couple of things to reverse engineer the ARM template via the Azure Portal.

First I tried the Automation Script option that is available for a Resource Group in the Azure Portal. Although this usually creates a bloated ARM template it was worth a shot. I created a new Resource Group containing a Web App Bot and clicked on Automation Script. Unfortunately I received the message that some resources could not be exported yet and were not included in the template:

"The schema of resource type 'Microsoft.BotService/botServices' is not available. Resources of this type will not be exported to the template. (Code: ResourceTypeSchemaNotFound, Target: Microsoft.BotService/botServices)".

The second option was to create a Web App Bot via the Azure Portal, but instead of creating the resource click on the Automation options to create an ARM template.

Use Azure Portal Automation options to create ARM template

Use Azure Portal Automation options to create ARM template

Luckily this approach was more successful. It created a convoluted template with a lot of unnecessary code, but at least it was something that I could work with. After some trial and error I got the lean result I wanted with the following:

Add to parameters:

"msaAppId": {
  "type": "string"
},
"botServiceSkuName": {
  "type": "string",
  "defaultValue": "F0",
  "allowedValues": [
    "F0",
    "S1"
  ]
}

The msaAppId parameter will contain the value of the Microsoft App Id. We will see how to get this value when we create the Release pipeline.

Add to variables:

"siteHost": "[concat(parameters('webAppName'), '.azurewebsites.net')]",
"botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]"

Add to resources:

{
  "apiVersion": "2017-12-01",
  "type": "Microsoft.BotService/botServices",
  "name": "[parameters('webAppName')]",
  "location": "global",
  "kind": "sdk",
  "sku": {
    "name": "parameters('botServiceSkuName')]"
  },
  "properties": {
    "name": "[parameters('webAppName')]",
    "displayName": "[parameters('webAppName')]",
    "endpoint": "[variables('botEndpoint')]",
    "msaAppId": "[parameters('msaAppId')]",
    "developerAppInsightsApplicationId": "[parameters('appInsightsName')]",
    "developerAppInsightKey": "[reference(resourceId('microsoft.insights/components/', parameters('appInsightsName')), '2015-05-01').InstrumentationKey]"
  },
  "dependsOn": [
    "[resourceId('microsoft.insights/components/', parameters('appInsightsName'))]"
  ]
}

Location should be global instead of the location of the Resource Group we used for the other resources and kind should be sdk.

Add Language Understanding (LUIS)

Add to parameters:

"luisName": {
  "type": "string",
  "defaultValue": "bvubotv4luis"
},
"luisSkuName": {
  "type": "string",
  "defaultValue": "F0",
  "allowedValues": [
    "F0",
    "S0"
  ]
}

Add to resources:

{
  "apiVersion": "2017-04-18",
  "type": "Microsoft.CognitiveServices/accounts",
  "name": "[parameters('luisName')]",
  "location": "[resourceGroup().location]",
  "kind": "LUIS",
  "sku": {
    "name": "[parameters('luisSkuName')]"
  },
  "properties": {}
}

That’s it! We’ve added all required parameters and resources to the ARM template. Now we could use it in an Azure DevOps Release pipeline to automatically provision the required resources for our Bot in Azure. Before we start with that, there is one more thing to add to the ARM template, namely the Outputs. In the Outputs section, we can specify values that are returned from the deployment.

After we have provisioned the resources and have deployed our Bot we will need to update the Bot Services (via the Bot’s configuration file) to use the services we just created like LUIS and Application Insights. To be able to update the Bot Services we need to know the value of some of their properties, like id’s, keys and endpoints. These values are created during the provisioning process and we can add them to the ARM Outputs section to make them available for tasks in our Release Pipeline.

Add the following values to the Output section:

"webAppName": {
  "type": "string",
  "value": "[parameters('webAppName')]"
},
"luisKey1": {
  "type": "string",
  "value": "[listKeys(variables('luisId'),'2016-02-01-preview').key1]"
},
"botEndpoint": {
  "type": "string",
  "value": "[variables('botEndpoint')]"
},
"appInsightsInstrumentationKey": {
  "type": "string",
  "value": "[reference(resourceId('Microsoft.Insights/components', parameters('appInsightsName')), '2014-04-01').InstrumentationKey]"
}

Also add the luisId variable to the variables section:

"luisId": "[concat(resourceGroup().id,'/providers/','Microsoft.CognitiveServices/accounts/', parameters('luisName'))]"

Updating Continuous Integration Build

Now that we have our ARM template we need to add it to the artifacts of our Continuous Integration Build to make it available in the Release Pipeline we are about to create.

Edit the CI Build and add a Copy Files task before the Publish Artifact task. The Source Folder field should point to the folder containing the ARM template. Add **.json to the Contents field to only copy JSON files to the Target Folder, which should be the Artifacts Staging Directory of the build.

Copy ARM template to artifacts

Copy ARM template to artifacts

Hit Save & Queue to trigger a new build. Check the artifacts for the ARM template and Parameters file after the build completes.

ARM template and Parameter files are added to the build artifacts

ARM template and Parameter files are added to the build artifacts

Microsoft Application Registration

We need a Microsoft Application Id and Password to be able to use the Bot with the Bot Service. When creating a Web App Bot from the Azure Portal the Microsoft Application Id and Password will be created automatically, but in our case we need to do this manually on the Microsoft Application Registration Portal. Login and click on Add an App. Enter a name for the app and hit Create.

Register the Bot at the Application Registration Portal

Register the Bot at the Application Registration Portal

This will take you to the Registration page where we can find the Application Id. On the same page under Application Secrets click on Generate New Password to create a password for the application.

App registration page with application Id and password

App registration page with application Id and password

Take a note of the Application Id and Password, we will use both in the Release Pipeline.

Adding Continuous Deployment

Create a Release Pipeline

Create a new Release Pipeline in Azure DevOps Pipelines and choose to start an Empty job.

Start the release from an empty job

Start the release from an empty job

First we want to add an artifact to the release. This will be the most recent result of the CI Build we created earlier. Adding this artifact will make the zip containing the Bot files and the ARM template available in the release.

Add the artifact created in the build

Add the artifact created in the build

Select the latest build from the CI build created earlier.

Select the latest build from the CI build

Select the latest build from the CI build

Click on the continuous deployment trigger (the lightning symbol in the upper-right corner of the artifact), enable the trigger and select a build branch. This will trigger a release every time a build on that branch completes.

Enable the continuous deployment trigger

Enable the continuous deployment trigger

Now rename the release pipeline and save it before we start adding tasks to the pipeline.

Add tasks to the release pipeline

Click on the� view stage tasks link in the already added stage (by default named ‘Stage 1') to start adding tasks.

Task 1: Azure Resource Group Deployment

Before we add the task we will store the name of the Resource Group we are about to create as a pipeline variable. We will need to enter the name of the Resource Group in multiple steps and defining it in one place reduces the risk of typos. Click on the variables menu item and add a new name value pair (f.e. ResourceGroupName – RSG-MYBOT).

Since we are on the variables page let’s also add variables for the parameters in the ARM template without default values:

  • BotFileSecret (value from appsettings.json (or secrets.json))
  • BotFilePath (value from appsettins.json (or secrets.json))
  • MicrosoftApplicationId (the Application Id of the created app in the Microsoft Application Registration Portal)

Also add the following variables:

  • MicrosoftApplicationPassword (the Application Password of the created app in the Microsoft Application Registration Portal)
  • EndpointId (the Id of the production endpoint defined in the .bot file)
  • LuisServiceId (the Id of the LUIS service defined in the .bot file)
  • AppInsightsServiceId (the Id of the Application Insights service defined in the .bot file)
Add pipeline variables

Add pipeline variables

Add the Azure Resource Group Deployment task to the stage.

Add the Azure Resource Group Deployment task

Add the Azure Resource Group Deployment task

Select the task and edit the settings. In the Resource group field we use the value of the ResourceGroupName variable. In the template field select the ARM template from the build artifact. In the Override template parameters field we use the defined variables to set values for the parameters with missing (default) values.

Azure resource group deployment task settings

Azure resource group deployment task settings

Oke, that should be enough to do our first release. Save the pipeline and create a new release. After the release has finished successfully we can check if all the resources we defined in the ARM template are actually created. Go to the Azure portal and validate the Resource group has been created with all the resources in it.

Azure resources created in with the release

Azure resources created in with the release

Also make sure the App Settings (BotFileSecret, BotFilePath and WEBSITE_NODE_DEFAULT_VERSION) of the App Service have the expected values. We defined the settings in the ARM template and used the pipeline variables to provide a value.

Remember the properties for the Bot Service we defined in the ARM template?

"properties": {
  "name": "[parameters('webAppName')]",
  "displayName": "[parameters('webAppName')]",
  "endpoint": "[variables('botEndpoint')]",
  "msaAppId": "[parameters('msaAppId')]",
  "developerAppInsightsApplicationId": "[parameters('appInsightsName')]",
  "developerAppInsightKey": "[reference(resourceId('microsoft.insights/components/', parameters('appInsightsName')), '2015-05-01').InstrumentationKey]",
}

We can now find them in the Bot Service (Web App Bot) settings blade. Under Configuration we see that the endpoint has the value of the ‘botEndpoint' variable we defined in the ARM template. The Microsoft App Id must be the value of the ‘msaAppId' parameter that has no value in the template, but was overridden with the MicrosoftApplicationId variable that we defined in the pipeline. Under Analytics we should see the properties of the created Application Insights resource (Instrumentation Key, API key and Application Id), because of the ‘developerAppInsightsApplicationId’ and developerAppInsightsApplicationKey' properties.

When we open the deployments blade of the created resource group and select the deployment, we should see the values of the outputs we defined in the ARM template. We need these values when we deploy our Bot to the App Service, so in the next step in the release pipeline we will make them available to the pipeline tasks.

Task 2: ARM Outputs

We could write some Powershell ourselves to make the output values ​​of the implementation available in the pipeline, but fortunately there are some extensions available in the marketplace that can do that for us. I used the ARM Outputs extension created by Kees Schollaart. This extension reads the output values of an ARM deployment and makes them available as variables in our pipeline.

Click on the plus sign to add another task to the release pipeline, search for ARM Outputs and install the extension.

Install the ARM Outputs extension from the marketplace

Install the ARM Outputs extension from the marketplace

Once installed, add the task to the pipeline.

Now select the Azure subscription and once again use the ResourceGroupName variable as value for the Resource Group field. It’s not required, but you can also provide a prefix value. This will add the prefix to the output name and makes it easier to distinguish between the output variables and the pipeline variables.

Adding the ARM Outputs task

Adding the ARM Outputs task

Task 3: Azure App Service Deploy

Now it’s time to deploy the Bot to the created Azure resources. Add an Azure App Service Deploy task to the release pipeline. Select your Azure subscription and Web App for the App type field. For App Service Name we will need to use the name of the App Service we created with our ARM template. We added the name to the ARM output values and is made available as a variable in the previous task. Enter $(out-webAppName) as the value. Select the zip file from the linked artifacts for the Package or folder field or leave the default value of $(System.DefaultWorkingDirectory)/**/*.zip, because we only have one zip file in our artifacts.

Add Azure App Service Deploy task

Add Azure App Service Deploy task

That in itself is enough to deploy the Bot to Azure. But the Bot will not be configured correctly. We still need to change some Bot settings (in the .bot file) like the endpoint and configure it to use the external services (LUIS, Application Insights) we created with the help of the ARM template. We used the MSBot CLI in the first part of this blog post to do this for the Bot running on our local development environment. We are going to do the same in this release pipeline.

In the Azure App Service Deploy task we can define a script that will run on the App Service after the task has completed the deployment successfully. We can use this script to run the MSBot commands. Open the Post Deployment Action section and select Inline script for the script type. The MSbot CLI will not be installed on a new instance of an App Service so we need to do that first by adding the following line to the Inline script field:

call npm install -g msbot

Now the MSBot CLI should be installed on the App Service and we can start adding MSBot commands. Add these lines to update the Bot’s external services:

call msbot update luis --id $(LuisServiceId) --authoringKey $(out-luisKey1) --secret $(BotFileSecret)
call msbot update appinsights --id $(AppInsightsServiceId) --resourceGroup $(ResourceGroupName) --instrumentationKey $(out-appInsightsInstrumentationKey) --secret $(BotFileSecret)

This will update the LUIS and Application Insights service in the .bot file with the keys from the Azure resources we have created with the ARM template. Key1 from LUIS Cognitive Service and the AuthoringKey from Application Insights. As you can see all parameter values are provided by the output or pipeline variables.

And of course don’t forget to update the production endpoint for the Bot:

call msbot update endpoint --secret $(BotFileSecret) --id $(EndpointId) --endpoint $(out-botEndpoint) --appId $(MicrosoftApplicationId) --appPassword $(MicrosoftApplicationPassword)

This will update the Bot’s production endpoint service in the .bot file. It will set the endpoint (based on the url of the created App Service) and the Microsoft Application Id and Password.

The release pipeline should be ready now. But when I ran a new release I got an error in the Azure App Service Deploy task stating that 'msbot' is not recognized as an internal or external command.

Although the logs state that MSBot is correctly installed, the MSBot command is somehow not recognized.

D:\home\site\wwwroot>call npm install -g msbot  
D:\local\AppData\npm\msbot -> D:\local\AppData\npm\node_modules\msbot\bin\msbot.js
+ msbot@4.0.7
added 100 packages in 12.603s

Standard error from script: 
##[error]'msbot' is not recognized as an internal or external command,

I’m not sure what causes this error but I got around it be changing the NodeJS version in the App Settings field in the Application and Configuration Settings section of the Azure App Service Deploy task:

-WEBSITE_NODE_DEFAULT_VERSION 8.11.1

The issue doesn’t seem to be related to the used NodeJS version. Defining WEBSITE_NODE_DEFAULT_VERSION to 8.11.1 instead of 8.9.4 in the ARM template results in the same error. Switching to a different version (f.e. 8.9.4) in the Azure App Service Deploy task fixes it again.

Save the release pipeline and create and deploy a new release to test the pipeline. If all went well the Bot is deployed and configured correctly.

Updating LUIS project

The LUIS project still uses the free endpoint. We want to replace that with the endpoint of the created LUIS resource in Azure. Unfortunately this can not be achieved in an automated way:

In order to assign the endpoint key to a LUIS app, you must use the LUIS website for the correct authoring and publishing regions. There is no automated method of doing this, regardless of mechanism such as with an Azure resource manager script, Azure CLI, programmatic SDK, or with the APIs.
Microsoft Docs

That means we can not add this action to the release pipeline and have to add the endpoint manually on the LUIS website. Navigate to Keys and Endpoints on the Manage page and click on the Assign resource button.

Assign a new LUIS resource

Assign a new LUIS resource

Select the tenant, subscription and the LUIS resource created by the release pipeline.

Select the LUIS resource created by the release pipeline

Select the LUIS resource created by the release pipeline

After you click Assign Resourcethe resource will be visible in the resource overview table.

Luis resources overview

Luis resources overview

We used the value of Key1 as the AuthoringKey when we ran the update LUIS MSBot command. Now everything should be hooked up correctly.

Let’s see if it worked!

Testing the Bot

Go to the Azure portal, select the Web App Bot and open the Test in Web Chat blade. Enter some text and see if the Bot responds in the same manner as on your local development environment. If you followed the first part of this blog post and used the Bot Builder Sample project you should see the following response:

Testing the Bot in the Test in Web Chat blade

Testing the Bot in the Test in Web Chat blade

When we open Application Insights we should also see this chat activity being logged as Custom Events.

Bot activity logged as Custom Events in Application Insights

Bot activity logged as Custom Events in Application Insights

Summary

In this second part of the blog post covering Continuous Deployment for Bots we created an ARM template that we used in an Azure DevOps release pipeline to automatically provision the required resources in Azure. Then we used the same pipeline to deploy the Bot and configure it to use the created resources like LUIS and Application Insights.

In this two part blog post we managed to automate to process of building and deploying a Bot to Azure. Now, all we need to do is push our code changes and Azure DevOps pipelines will take care of the rest. I hope you found it useful!

comments powered by Disqus