Managing secrets in the application is crucial part of the whole development process. Please look at the picture. There are two loops:

Inner - Focused on the developer teams iterating over their solution development (they consume the configuration published by the outer loop)

Outer - The Ops Engineer govern the Configuration management and push changes (including Azure KeyVault secrets management)

With such approach you are able keep clear separation of concerns and clean code. What is more, application configuration is much easier to maintain.

In this article I would like to present how integrate Azure Key Vault with Azure DevOps Release pipelines and how to inject secrets per specific environment (in this case Development, QA and Production).

Prerequisites

Before we start I assume that there are already three separate resource groups created for each enfironment:

dev-island-app-dev-rg: for Development

dev-island-app-qa-rg: for QA

dev-island-app-prod-rg: Production

Of course there resource groups creation can be also automated. But for this article I created them manually. In each of above resource group I created Key Vault:

dev-island-app-dev-kv

dev-island-app-qa-kv

dev-island-app-prod-kv

I created separate Key Vault per environment because it is recommended approach by Microsoft:

“Our recommendation is to use a vault per application per environment (Development, Pre-Production and Production).”

You can read more here.

In each Key Vault I created one secret with different values for each environment:

Name: “DbConnectionString”

Value for the Development environment: “Server=(localdb)\mssqllocaldb;Database=CarsIsland;Trusted_Connection=True;”

Value for the QA environment: “Server=(localdb)\mssqllocaldb;Database=CarsIslandQA;Trusted_Connection=True;”

Value for the Production environment: “Server=(localdb)\mssqllocaldb;Database=CarsIslandProd;Trusted_Connection=True;”

Add new service connection so you can access Azure resources from the Azure DevOps

Prepare release pipeline with Development, QA and Production stages

First of all we have to prepare release pipeline for all three environments: Development, QA and Production. Follo below steps:

Setup Azure Key Vault integration in the Release pipeline

First of all we have to integrate Key Vault in the Release pipeline so secrets are available through variable group. Each stage in the release pipeline has its own variable group. Lets see how to do it.

Setup variable group for the Development environment

Select “Manage variable groups”:

Click “+ Variable group”:

Provide details about this specific variable group:

You have to auhorize Azure DevOps to access Azure subscription and Key Vault:

Now select which secrets you would like to use as variable in the release pipeline:

In our case we have to select the secret created before called “DbConnectionString”:

Once you select the secret, click “Save” button:

Now get back to the “variable groups” tab and click “Link variable group”:

Select the group we created above so “dev-island-app-dev-kv-vg “. Set “Variable group scope” to “Stages” and select only “Development”:

Click “Save” button:

Repeat above steps for the QA and Production environments

Create variable groups for the QA and Production like presented below:

dev-island-app-qa-kv-vg

dev-island-app-prod-kv-vg

Finally it looks like below:

Below three variable groups should be linked:

Setup Development environment release

We will use ARM template to deploy sample web app with the database connection string added to the configuration. For the Development environment we would like to use database connection string from the “dev-island-app-dev-kv-vg” variable group.

“azuredeploy.json” file looks like below:

{ "$schema" : "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" , "contentVersion" : "1.0.0.0" , "parameters" : { "TestAppServicePlanName" : { "type" : "string" , "minLength" : 1 }, "TestWebAppName" : { "type" : "string" , "minLength" : 1 }, "DbConnectionString" : { "type" : "string" , "minLength" : 1 }, "TestAppServicePlanSkuName" : { "type" : "string" , "defaultValue" : "D1" , "allowedValues" : [ "F1" , "D1" , "B1" , "B2" , "B3" , "S1" , "S2" , "S3" , "P1" , "P2" , "P3" , "P4" ], "metadata" : { "description" : "Describes plan's pricing tier and capacity. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/" } } }, "variables" : { "TestWebAppName" : "[concat(parameters('TestWebAppName'), uniqueString(resourceGroup().id))]" }, "resources" : [ { "name" : "[parameters('TestAppServicePlanName')]" , "type" : "Microsoft.Web/serverfarms" , "location" : "[resourceGroup().location]" , "apiVersion" : "2015-08-01" , "sku" : { "name" : "[parameters('TestAppServicePlanSkuName')]" }, "dependsOn" : [], "tags" : { "displayName" : "TestAppServicePlan" }, "properties" : { "name" : "[parameters('TestAppServicePlanName')]" , "numberOfWorkers" : 1 } }, { "name" : "[variables('TestWebAppName')]" , "type" : "Microsoft.Web/sites" , "location" : "[resourceGroup().location]" , "apiVersion" : "2015-08-01" , "dependsOn" : [ "[resourceId('Microsoft.Web/serverfarms', parameters('TestAppServicePlanName'))]" ], "tags" : { "[concat('hidden-related:', resourceId('Microsoft.Web/serverfarms', parameters('TestAppServicePlanName')))]" : "Resource" , "displayName" : "TestWebApp" }, "properties" : { "name" : "[variables('TestWebAppName')]" , "serverFarmId" : "[resourceId('Microsoft.Web/serverfarms', parameters('TestAppServicePlanName'))]" , "siteConfig" : { "connectionStrings" : [ { "name" : "DbTestConnectionString" , "connectionString" : "[parameters('DbConnectionString')]" , "type" : "string" } ] } }, "resources" : [ { "name" : "appsettings" , "type" : "config" , "apiVersion" : "2015-08-01" , "dependsOn" : [ "[resourceId('Microsoft.Web/sites', variables('TestWebAppName'))]" ], "tags" : { "displayName" : "TestWebAppSettings" }, "properties" : { "key1" : "value1" , "key2" : "value2" } } ] } ], "outputs" : {} }

“azuredeploy.parameters.json” file looks like below:

{ "$schema" : "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#" , "contentVersion" : "1.0.0.0" , "parameters" : { "TestAppServicePlanName" : { "value" : "TestAppServicePlan" }, "TestWebAppName" : { "value" : "TestWebAppCreatedWithARM" }, "DbConnectionString" : { "value" : "#" } } }

Please note that in the section called “Override template parameters” we have to replace parameter with the value for the database connection string from the Key Vault:

- DbConnectionString $ ( DbConnectionString )

Setup QA environment release

For the QA environment we will have the same steps like presented above. In this case we are creating web app in the QA resource group in the Azure:

Setup Production environment release

For the Production environment we will have the same steps like presented above. In this case we are creating web app in the Production resource group in the Azure:

Create new release and check web apps configuration

Create new release. Once web app is created in each of the resource groups, check “Configuration” tab. You should see that each app has different database connection string retrieved from the Key Vault related with specific environment:

Summary

In this article I explained how to inject Azure Key Vault secrets in the Azure DevOps release pipelines for multiple environments using variable groups. I hope you will find it valuable. Of course instead of variable groups you could use “Add Azure Key Vault” task in the release definition but I wanted to show another approach.