DevOps for LUIS - Part 4

In this series of blog posts, we create a CI/CD pipeline in Azure DevOps for a LUIS language model. In the pipeline, we will subject the model to various types of tests, after which we will ‘build’ and deploy it to an Azure environment.

In this last blog post of the series we will deploy the LUIS language model to an Azure environment.

The entire series consists of the following blog posts:

The solution containing the Azure DevOps Yaml pipeline and all files used in the pipeline is available in this GitHub Repo.


In this post, we’ll pick up where we left off the last post. So if you haven’t read my previous blog post, I recommend that you do this before reading on. In the already completed stages we have created a build artifact that is now available in the pipeline. In addition, the VersionId is also available as an environment variable. Both are required in the Deploy stages that we will be adding to the pipeline in this post.


As already explained in the previous post, we use templates to reuse steps that occur multiple times in the pipeline.

Install-tools template

This template was covered in the previous post. It installs the Microsoft Bot Framework CLI (BF CLI) and the NLU.DevOps CLI. However, we will only use the BF CLI in the deploy stages.

At the time of writing the BF CLI supports Node version 12. If your build agent is using a newer node version you may get this error when running a BF CLI command: ##[error](node:1932) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency This is a known issue and a fix is scheduled for the next release. In the meantime, you can get around this by explicitly stating that you want to use version 12 in the template.

- task: NodeTool@0
    versionSpec: '12.x'
  displayName: Use Node version 12.x

Deploy template

We create a new template called deploy and add the necessary steps to deploy the model. We will reuse this template in all the deploy stages of the pipeline. We are going to take a closer look at this template and the steps described in it a little further in this post.

QA stage

We add a new stage in which we will deploy to the first environment. I have chosen a QA environment here, but this can be anything depending on your situation, for instance Develop, Test or Acceptance if you use traditional DTAP environments. I named the stage after the environment QA.


  LuisVersion: $[ stageDependencies.Build.CiBuildJob.outputs['Versiontask.luisVersion'] ]
  ResourceGroupName: 'bvu-blog-luis-qa'
  LuisAuthoringResourceName: 'bvu-chat-luis-authoring-qa'
  LuisPredictionResourceName: 'bvu-chat-luis-prediction-qa'
  LuisApplicationName: 'chat-qa'

LuisVersion is the versionId of the LUIS model we are going to deploy. We get it’s value from the output variable (luisVersion) we have set in the final step of the Build stage. To use the output from a different stage we need to use the stageDependencies syntax. The stage, job, task and variable name are used to refer to that variable. The other variables are for the Azure resources and resource group and the LUIS application name.


  - deployment:
    displayName: 'Deployment'
      vmImage: $(VmImage)
    environment: QA

A Deployment job is used in the deploy stages. For YAML pipelines it is recommended to perform deployment actions in a deployment job. The deployment actions are performed on the specified environment according to the specified strategy. The strategy is not that important for this pipeline, so we pick the simplest one, runOnce.


Under steps we just add the two templates. All necessary steps to deploy the model are in these two templates.

  - template: templates\install-tools.yml
  - template: templates\deploy.yml

Let’s take a closer look at the steps in the deploy template.

Deploy template

The first step is a AzureCLI task in which we run different commands to create the necessary Azure resources.

- task: AzureCLI@2
  displayName: Create LUIS resources
    azureSubscription: $(AzureSubscription)
    scriptType: 'ps'
    scriptLocation: 'inlineScript'
    inlineScript: |
      # create resources
      az group create -l $(LuisLocation) -n $(ResourceGroupName)
      az cognitiveservices account create -n $(LuisAuthoringResourceName) -g $(ResourceGroupName) -l $(LuisLocation) --kind LUIS.Authoring --sku F0
      az cognitiveservices account create -n $(LuisPredictionResourceName) -g $(ResourceGroupName) -l $(LuisLocation) --kind LUIS --sku S0

      # get keys
      $authoringKey = $(az cognitiveservices account keys list -n $(LuisAuthoringResourceName) -g $(ResourceGroupName) --query key1)
      $predictionKey = $(az cognitiveservices account keys list -n $(LuisPredictionResourceName) -g $(ResourceGroupName) --query key1)

      # set environment variables
      Write-Host "##vso[task.setvariable variable=luisPredictionKey]$predictionKey"
      Write-Host "##vso[task.setvariable variable=luisAuthoringKey]$authoringKey"

First we create the resource group in which we create the LUIS Authoring and Prediction resource. Then we create two new environment variables and set them with the key values.

In the next step we will add these, together with previously created environment variables, to the BF CLI configuration.

- script: bf config:set:luis --authoringKey $(luisAuthoringKey) --endpoint $(LuisEndpoint) --subscriptionKey $(luisAuthoringKey) --versionId $(LuisVersion)
  displayName: Set LUIS config for Bot Framework CLI
  failOnStderr: true

This will set default values ​​for frequently used command options. The tool can read these back out from the config when they are required for a command. This means that we do not have to explicetly add them to commands that require them.

We will make good use of this in the next step, where we create a new LUIS application

- script: bf luis:application:create --name $(LuisApplicationName)
  displayName: Create LUIS application
  continueOnError: true # task will fail if application already exists

Normally you should also add the endpoint and subscriptionkey options here, but since we just added these to the config this is no longer necessary. So we can write this:

bf luis:application:create --name $(LuisApplicationName)

instead of this:

bf luis:application:create --name $(LuisApplicationName) --endpoint $(LuisEndpoint) --subscriptionKey $(luisAuthoringKey)

Unfortunately the tool returns an error if the application with the given name already exists. Therefore, when deploying a new version for an existing application, the build will fail. To prevent that, we set continueOnError to true. You can of course also choose to disable this step after the application has been created for the first time.

Now we need to get the AppId from the LUIS application we just created. We need that id when importing, training and publishing the model. In the previous post we just went to the portal to look this up, but now we will automate this.

- powershell: |
    $jsonString = bf luis:application:list
    $json = $jsonString | out-string | ConvertFrom-Json
    $appId = $json[0].id
    Write-Host $appId
    # set BF config appId option
    bf config:set:luis --appId $appId
  displayName: Get AppId and set BF config appId option

First, we get the list of LUIS applications and convert the JSON string to a anonymous object using the ConvertFrom-Json Powershell cmdlet. Since we only have one application, we take the id of the first item in the array. Then, we set it as a default value for the appId option of the BF CLI configuration.

We are now ready to import and train the model, but before we can publish it, we must first add a LUIS prediction resource to the application. This will be the prediction resource we created in the first step of this deploy template.

- task: AzureCLI@2
    azureSubscription: $(AzureSubscription)
    scriptType: 'ps'
    scriptLocation: 'inlineScript'
    inlineScript: |
      $token = $(az account get-access-token --query 'accessToken' -o tsv)      
      bf luis:application:assignazureaccount --accountName $(LuisPredictionResourceName) --azureSubscriptionId $(AzureSubscriptionId) --resourceGroup $(ResourceGroupName) --armToken "$token"
  displayName: Add prediction resource to application

We can use another BF CLI command to assign the LUIS prediction resource to our application. This command requires an option we haven’t seen before in this CLI, which is the armToken option. We can get an arm token by using the az account get-access-token Azure CLI command. Make sure to run this command in an AzureCLI task. This will properly authenticate you to run the command. Then we run the bf luis:application:assignazureaccount command to assign the LUIS prediction resource (--accountName) to the LUIS application (--AppId, defaults to config:LUIS:appId).

Now we can import the model to the application.

- script: bf luis:version:import --in $(Pipeline.Workspace)\LuisApp\
  displayName: Import LUIS app (lu) from artifact
  failOnStderr: true # task will fail if any errors are written to stderr, f.e. when version already exists

The application contents (LU file) is available in the LuisApp build artifact we created in the previous post. Once imported we can train the LUIS app.

- script: bf luis: train:run --wait
  displayName: Train LUIS app
  failOnStderr: true

Training is asynchronous so we have to add the wait flag to avoid starting the next task before the training is complete. And the final step is to publish the trained app to the prediction endpoint.

- script: bf luis:application:publish
  displayName: Publish to LUIS production endpoint
  failOnStderr: true

Now the LUIS model is available on the QA environment and requests can be made to the prediction endpoint. If you want to make sure all went well, you can go the the LUIS portal (manage > azure resources > prediction resource) and make a request using the example query.

PRD stage

We can now easily use the QA stage as a basis to expand the pipeline by adding additional stages that deploy to other environments. For example, the setup of the PRD stage in the pipeline is almost identical to the QA stage. Only the values ​​for the variables are different.

LuisVersion: $[ stageDependencies.Build.CiBuildJob.outputs['Versiontask.luisVersion'] ]
ResourceGroupName: 'bvu-blog-luis-prd'
LuisAuthoringResourceName: 'bvu-chat-luis-authoring-prd'
LuisPredictionResourceName: 'bvu-chat-luis-prediction-prd'
LuisApplicationName: 'chat-prd'

And we have added an extra condition in which we indicate that only the main branch may be used for deployments to production.

condition: and(succeeded(), eq(variables['build.sourceBranch'], 'refs/heads/main'))


That’s it for this series of blogs about DevOps for LUIS. Hopefully I have been able to give you a good idea of ​​how you can automatically test, build and deploy a LUIS model. I’d love to hear from you if you have any questions or if you were able to use this information in one of your own projects. You can let me know in the comments or reach out to me on Twitter or LinkedIn.



Bot Framework CLI:

Azure DevOps yaml pipelines: * Pipeline deployment jobs * Use outputs in different stage

comments powered by Disqus