The ability to use the Azure DevTest labs within a development inner loop and testing has been documented , but this post will look at how the DevTest labs can be used in the Azure DevOps build and release pipelines. The basic flow is to have a build pipeline to that will build the application code, create the necessary environment in DevTest Labs, deploy the application to the DevTest Lab environment, then test the code. Once build has been completed successfully, the release pipeline will use the build artifacts to deploy staging, or production. One of the necessary premises is that all the information needed to recreate the “tested” ecosystem is available within the build artifacts, including the configuration of the Azure resources. As Azure resources incur a cost when used, companies tend to want to either control or track the use of these resources. In some situations, the Azure RM templates that are used to create and configure the resources may be managed by another department such as IT, which could store them in a different repository. This leads to an interesting situation where a build will be created and tested, and both the code and the configuration will need to be stored within the build artifacts to properly recreate the system in production. Using DevTest Labs during the build/test phase, we can add the correct ARM templates and supporting files to the build sources, so that during the release phase the exact configuration used in testing is deployed to production. The “Create Azure DevTest Labs Environment” task with the proper configuration will save the ARM templates within the build artifacts. For this example I’ll be using the code from the Tutorial: Build a .NET Core and SQL Database web app in Azure App Service, to deploy and test the web app in Azure.
Code and Configuration in separate repositories
Overall Flow
![]()
Setup Azure Resource
There are a couple of items that will need to be created beforehand:
- Two repositories, the first with the code from the tutorial, the second will contain the ARM template (configuration).
- A Resource Group for deployment of the production code and configuration.
- A DevTest Lab (TestLab) will need to be setup with a connection to the configuration repository for the build pipeline. I’ve included the necessary ARM template that will create the Web App and SQL Server to support the Tutorial: Build a .NET Core and SQL Database web app in Azure App Service. The ARM template will need to be checked into the configuration repository as azuredeploy.json with the metadata.json to allow DevTest lab to recognize and deploy the template.
The DevTest Lab is where the build pipeline will create the environment and deploy the code for testing.
Setup Build pipeline
In Azure DevOps, create a new build pipeline using the code from the Tutorial: Build a .NET Core and SQL Database web app in Azure App Service using the “ASP.NET Core” template which will populate the necessary task to build, test, and publish the code.
![]()
Two additional tasks will need to be added to create the environment in DevTest Lab and deploy to the environment.
![]()
The “Create Azure DevTest Labs Environment” task before the “Test” task. In the “create environment” task, use the pulldowns to select the appropriate Azure RM Subscription, Lab Name, Repository Name, and Template Name (which shows the folder name where the environment is stored). I would highly recommend using the pulldowns; if you manually enter the information, you will need the fully qualified Azure Resource Id for this task to work. The task displays the “friendly” names instead of the resource IDs. The environment name is the displayed name shown within DevTest labs, this should be a unique name for each build ie “TestEnv$(Build.BuildId)”. Either the Parameters File or the Parameters section can be used to pass information into the ARM template – see Additional information / Azure Resource Management Parameters for an example. The “Create output variables based on the environment template output?” to allow the output to be recognized by the build pipeline. The “Create artifact based on the environment template output?” will need to be enabled with the appropriate output names to allow the ARM templates to be downloaded to the build artifact. This can be done through the advanced option.
![]()
The second task is the “Azure App Service Deploy”, which will be added after the Create task above. The App type will be “Web App” and the App Service name set to $(WebSite) to deploy the app to the app service within the DTL Environment that was created.
![]()
Setup Release pipeline
In the release pipeline, the two tasks are the “Azure Deployment: Create Or Update Resource Group action” and “Deploy Azure App Service”. The Create of Update Resource Group Action will need:
- The Azure Subscription where the production resource group is located.
- The action will be “Create or update resource group”.
- The name of the Resource Group.
- The location of the Resource Group.
- The template location is a “linked artifact”.
- The template is “$(System.DefaultWorkingDirectory)/_<Build Name>/drop/DeployedEnvironment/azuredeploy.json”.
- The Override template parameters for the ARM template – see Additional information / Azure Resource Management Parameters for an example.
The rest of the options can be left with the defaults. If the ARM template includes linked templates then a custom resource group deployment will need to be implemented. The second task “Deploy Azure App Service” will need the Azure Subscription, the App type will be Web App; and the App Service name we’ve setup as $(WebSite). The rest can be left to the defaults.
Test Run
Now that both pipelines are set up, manually queue up a build and see it work. The next step is to set the appropriate trigger for the build and connect the build to the release pipeline.
Have a question? Check out the answers or ask a new one at MSDN forum.
![]()
Roger Best, Senior Software Engineer
Roger is part of the Visual Studio and .NET engineering team focused on Visual Studio and Azure customers. He has been at Microsoft for over 20 years, focusing on developer technologies for the past decade or so. In his spare time, he watches too many movies, and tries to survive triathlons
Additional information
Demo Build / Release variables
- AdministratorLogin: Administrator Name
- AdministratorPassword: Administrator Password – secret type
- SqlDbName: SQL database name – lower case only
- SqlSrvName: SQL server name
- WebSite: App Service name
Azure Resource Management Parameters
-hostingPlanName 'hostplan$(Build.BuildId)' -webSiteName '$(WebSite)' -sqlServerName '$(SqlSrvName)' -administratorLogin '$(AdministratorLogin)' -administratorLoginPassword '$(AdministratorPassword)' -databaseName '$(SqlDbName)'
DevTest Lab Environment metadata information (metadata.json)
{
"itemDisplayName": "NET Core application with SQL Db",
"description": "This template creates an Azure Web App with SQL DB."
}
Azure ARM Template for Web App with SQL Server (azuredeploy.json)
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"hostingPlanName": {
"type": "string",
"minLength": 1
},
"webSiteName": {
"type": "string",
"defaultValue": "testwebapp"
},
"sqlServerName": {
"type": "string",
"defaultValue": "testsqlsrv"
},
"skuName": {
"type": "string",
"defaultValue": "F1",
"allowedValues": [
"F1",
"D1",
"B1",
"B2",
"B3",
"S1",
"S2",
"S3",
"P1",
"P2",
"P3",
"P4"
],
"metadata": {
"description": "Describes plan's pricing tier and instance size. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/"
}
},
"skuCapacity": {
"type": "int",
"defaultValue": 1,
"minValue": 1,
"metadata": {
"description": "Describes plan's instance count"
}
},
"administratorLogin": {
"type": "string"
},
"administratorLoginPassword": {
"type": "securestring"
},
"databaseName": {
"type": "string"
},
"collation": {
"type": "string",
"defaultValue": "SQL_Latin1_General_CP1_CI_AS"
},
"edition": {
"type": "string",
"defaultValue": "Basic",
"allowedValues": [
"Basic",
"Standard",
"Premium"
]
},
"maxSizeBytes": {
"type": "string",
"defaultValue": "1073741824"
},
"requestedServiceObjectiveName": {
"type": "string",
"defaultValue": "Basic",
"allowedValues": [
"Basic",
"S0",
"S1",
"S2",
"P1",
"P2",
"P3"
],
"metadata": {
"description": "Describes the performance level for Edition"
}
},
"_artifactsLocation": {
"type": "string",
"defaultValue": ""
},
"_artifactsLocationSasToken": {
"type": "securestring",
"defaultValue": ""
}
},
"variables": {
},
"resources": [
{
"name": "[parameters('sqlserverName')]",
"type": "Microsoft.Sql/servers",
"location": "[resourceGroup().location]",
"tags": {
"displayName": "SqlServer"
},
"apiVersion": "2014-04-01-preview",
"properties": {
"administratorLogin": "[parameters('administratorLogin')]",
"administratorLoginPassword": "[parameters('administratorLoginPassword')]"
},
"resources": [
{
"name": "[parameters('databaseName')]",
"type": "databases",
"location": "[resourceGroup().location]",
"tags": {
"displayName": "Database"
},
"apiVersion": "2014-04-01-preview",
"dependsOn": [
"[resourceId('Microsoft.Sql/servers/', parameters('sqlserverName'))]"
],
"properties": {
"edition": "[parameters('edition')]",
"collation": "[parameters('collation')]",
"maxSizeBytes": "[parameters('maxSizeBytes')]",
"requestedServiceObjectiveName": "[parameters('requestedServiceObjectiveName')]"
}
},
{
"type": "firewallrules",
"apiVersion": "2014-04-01-preview",
"dependsOn": [
"[resourceId('Microsoft.Sql/servers/', parameters('sqlserverName'))]"
],
"location": "[resourceGroup().location]",
"name": "AllowAllWindowsAzureIps",
"properties": {
"endIpAddress": "0.0.0.0",
"startIpAddress": "0.0.0.0"
}
}
]
},
{
"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')]"
}
},
{
"apiVersion": "2015-08-01",
"name": "[parameters('webSiteName')]",
"type": "Microsoft.Web/sites",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]"
],
"tags": {
"[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty",
"displayName": "Website"
},
"properties": {
"name": "[parameters('webSiteName')]",
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]"
},
"resources": [
{
"apiVersion": "2015-08-01",
"type": "config",
"name": "connectionstrings",
"dependsOn": [
"[resourceId('Microsoft.Web/Sites/', parameters('webSiteName'))]"
],
"properties": {
"DefaultConnection": {
"value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers/', parameters('sqlserverName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('databaseName'), ';User Id=', parameters('administratorLogin'), '@', parameters('sqlserverName'), ';Password=', parameters('administratorLoginPassword'), ';')]",
"type": "SQLServer"
}
}
}
]
},
{
"apiVersion": "2014-04-01",
"name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]",
"type": "Microsoft.Insights/autoscalesettings",
"location": "[resourceGroup().location]",
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
"displayName": "AutoScaleSettings"
},
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
],
"properties": {
"profiles": [
{
"name": "Default",
"capacity": {
"minimum": 1,
"maximum": 2,
"default": 1
},
"rules": [
{
"metricTrigger": {
"metricName": "CpuPercentage",
"metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
"timeGrain": "PT1M",
"statistic": "Average",
"timeWindow": "PT10M",
"timeAggregation": "Average",
"operator": "GreaterThan",
"threshold": 80.0
},
"scaleAction": {
"direction": "Increase",
"type": "ChangeCount",
"value": 1,
"cooldown": "PT10M"
}
},
{
"metricTrigger": {
"metricName": "CpuPercentage",
"metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
"timeGrain": "PT1M",
"statistic": "Average",
"timeWindow": "PT1H",
"timeAggregation": "Average",
"operator": "LessThan",
"threshold": 60.0
},
"scaleAction": {
"direction": "Decrease",
"type": "ChangeCount",
"value": 1,
"cooldown": "PT1H"
}
}
]
}
],
"enabled": false,
"name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]",
"targetResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
}
},
{
"apiVersion": "2014-04-01",
"name": "[concat('ServerErrors ', parameters('webSiteName'))]",
"type": "Microsoft.Insights/alertrules",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('webSiteName'))]"
],
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('webSiteName'))]": "Resource",
"displayName": "ServerErrorsAlertRule"
},
"properties": {
"name": "[concat('ServerErrors ', parameters('webSiteName'))]",
"description": "[concat(parameters('webSiteName'), ' has some server errors, status code 5xx.')]",
"isEnabled": false,
"condition": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
"dataSource": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
"resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('webSiteName'))]",
"metricName": "Http5xx"
},
"operator": "GreaterThan",
"threshold": 0.0,
"windowSize": "PT5M"
},
"action": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
"sendToServiceOwners": true,
"customEmails": []
}
}
},
{
"apiVersion": "2014-04-01",
"name": "[concat('ForbiddenRequests ', parameters('webSiteName'))]",
"type": "Microsoft.Insights/alertrules",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('webSiteName'))]"
],
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('webSiteName'))]": "Resource",
"displayName": "ForbiddenRequestsAlertRule"
},
"properties": {
"name": "[concat('ForbiddenRequests ', parameters('webSiteName'))]",
"description": "[concat(parameters('webSiteName'), ' has some requests that are forbidden, status code 403.')]",
"isEnabled": false,
"condition": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
"dataSource": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
"resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('webSiteName'))]",
"metricName": "Http403"
},
"operator": "GreaterThan",
"threshold": 0,
"windowSize": "PT5M"
},
"action": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
"sendToServiceOwners": true,
"customEmails": []
}
}
},
{
"apiVersion": "2014-04-01",
"name": "[concat('CPUHigh ', parameters('hostingPlanName'))]",
"type": "Microsoft.Insights/alertrules",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
],
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
"displayName": "CPUHighAlertRule"
},
"properties": {
"name": "[concat('CPUHigh ', parameters('hostingPlanName'))]",
"description": "[concat('The average CPU is high across all the instances of ', parameters('hostingPlanName'))]",
"isEnabled": false,
"condition": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
"dataSource": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
"resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
"metricName": "CpuPercentage"
},
"operator": "GreaterThan",
"threshold": 90,
"windowSize": "PT15M"
},
"action": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
"sendToServiceOwners": true,
"customEmails": []
}
}
},
{
"apiVersion": "2014-04-01",
"name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]",
"type": "Microsoft.Insights/alertrules",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]"
],
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource",
"displayName": "AutoScaleSettings"
},
"properties": {
"name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]",
"description": "[concat('The HTTP queue for the instances of ', parameters('hostingPlanName'), ' has a large number of pending requests.')]",
"isEnabled": false,
"condition": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition",
"dataSource": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource",
"resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]",
"metricName": "HttpQueueLength"
},
"operator": "GreaterThan",
"threshold": 100.0,
"windowSize": "PT5M"
},
"action": {
"odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction",
"sendToServiceOwners": true,
"customEmails": []
}
}
},
{
"apiVersion": "2014-04-01",
"name": "[parameters('webSiteName')]",
"type": "Microsoft.Insights/components",
"location": "East US",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('webSiteName'))]"
],
"tags": {
"[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('webSiteName'))]": "Resource",
"displayName": "AppInsightsComponent"
},
"properties": {
"ApplicationId": "[parameters('webSiteName')]"
}
}
],
"outputs": {
"EnvironmentLocation":{
"type": "string",
"value": "[parameters('_artifactsLocation')]"
},
"EnvironmentSAS":{
"type": "string",
"value": "[parameters('_artifactsLocationSasToken')]"
},
"appServiceName":{
"type": "string",
"value": "[parameters('webSiteName')]"
},
"sqlSrvName":{
"type": "string",
"value": "[parameters('sqlserverName')]"
}
}
}