When an application consists of multiple modules (i.e. with independent Resource Groups and deployments), the configuration of resources in other modules such as e.g. connection strings becomes a challenge. This snippet shows the possible strategies to resolve with minimal configuration complexity.
To resolve the challenge of passing configurations from other deployments, these patterns are sometimes implemented:
The possible use cases are shown with this sample architecture:
With the default creation mode, the KeyVault overrides all existing Access Policies with a new array. The creation mode recover
allows to keep all current definitions and implement an “upsert” like behaviour for deployments of Access Policies. But this mode fails for initial deployments. So we determine first if the KeyVault already exists.
templates.deploy-to-stage.yml
:
- task: AzureCLI@2
displayName: 'Evaluate variables for KeyVault deployment'
inputs:
azureSubscription: '$'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
keyVaultCount=$(az keyvault list --query "[?name=='$(keyVaultName)'] | length(@)" --subscription $(subscriptionId))
if [ $keyVaultCount -gt 0 ]; then
echo "##vso[task.setvariable variable=keyVaultExists;]true"
else
echo "##vso[task.setvariable variable=keyVaultExists;]false"
fi
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy ARM Template (ResourceGroup)'
inputs:
azureResourceManagerConnection: '$'
...
csmFile: '$(Pipeline.Workspace)/CI-Pipeline/$(ciArtifactName)/azuredeploy.base.bicep'
overrideParameters: '-resourceNamePrefix "$(baseResourceNamePrefix)" -resourceNameSuffix "$" -useExistingKeyVault $(keyVaultExists) -servicePrincipalId "$(armServicePrincipalId)"'
resource keyVaultRes 'Microsoft.KeyVault/vaults@2021-10-01' = {
name: keyVaultName
location: resourceLocation
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enabledForTemplateDeployment: true
enableRbacAuthorization: false
enableSoftDelete: true // With default of softDeleteRetentionInDays = 90
createMode: useExistingKeyVault ? 'recover' : 'default'
accessPolicies: []
}
}
Deployments to resources in another Resource Group are most easily made with Bicep modules (nested templates):
param keyVaultName string
param secretName string
@secure()
param secretValue string
resource keyVaultRes 'Microsoft.KeyVault/vaults@2021-10-01' existing = {
name: keyVaultName
}
resource keyVaultSecretRes 'Microsoft.KeyVault/vaults/secrets@2021-10-01' = {
parent: keyVaultRes
name: secretName
properties: {
value: secretValue
}
}
module keyVaultSecretSignalRConnectionStringRes './modules.keyVaultSecret.bicep' = if (!empty(keyVaultName) && !empty(keyVaultResourceGroupName)) {
name: keyVaultSecretSignalRConnectionString
scope: resourceGroup(keyVaultResourceGroupName)
params: {
keyVaultName: keyVaultName
secretName: keyVaultSecretSignalRConnectionString
secretValue: listKeys(signalRServiceRes.id, '2021-10-01').primaryConnectionString
}
}
The common reference syntax for App Services and Function Apps does not require the Key Vault being deployed to the same Resource Group or within the same deployment definition.
modules.funcAppSettings.bicep
:
resource serviceFuncAppSettingsRes 'Microsoft.Web/sites/config@2021-03-01' = {
parent: serviceFuncRes
name: 'appsettings'
properties: {
...
StorageConnectionString: '@Microsoft.KeyVault(VaultName=${keyVaultName};SecretName=${keyVaultSecretStorageAccountConnectionString})'
...
}
}
Deploying the Access Policy for the MSI (azuredeploy.application.bicep
):
module keyVaultAccessPolicyRes './modules.keyVaultAccessPolicy.bicep' = if (!empty(keyVaultName) && !empty(keyVaultResourceGroupName)) {
name: 'keyvault-accesspolicy-service'
scope: resourceGroup(keyVaultResourceGroupName)
params: {
keyVaultName: keyVaultName
principalId: reference(serviceFuncRes.id, '2021-03-01', 'Full').identity.principalId
}
}
modules.keyVaultAccessPolicy.bicep
:
param keyVaultName string
param principalId string
param appPermissions object = {
keys: [
'get'
]
secrets: [
'get'
]
}
resource keyVaultRes 'Microsoft.KeyVault/vaults@2021-10-01' existing = {
name: keyVaultName
}
resource keyVaultAccessPoliciesRes 'Microsoft.KeyVault/vaults/accessPolicies@2021-10-01' = {
parent: keyVaultRes
name: 'add'
properties: {
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: principalId
permissions: appPermissions
}
]
}
}
This is well documented in MSDN Bicep/ARM documentation. Valuable to know:
resource keyVaultRes 'Microsoft.KeyVault/vaults@2021-10-01' existing = if (!empty(keyVaultName) && !empty(keyVaultResourceGroupName)) {
name: keyVaultName
scope: resourceGroup(keyVaultResourceGroupName)
}
module serviceFuncAppSettingsRes './modules.funcAppSettings.bicep' = if (!empty(keyVaultName) && !empty(keyVaultResourceGroupName)) {
name: 'func-appsettings'
params: {
keyVaultName: keyVaultName
serviceFuncName: serviceFuncName
storageAccountConnectionString: keyVaultRes.getSecret(keyVaultSecretStorageAccountConnectionString)
...
}
dependsOn: [
keyVaultAccessPolicyRes
]
}
Gather AAD identifier of Service Principal (templates.deploy-to-stage.yml
):
- task: AzureCLI@2
displayName: 'Evaluate variables for KeyVault deployment'
inputs:
azureSubscription: '$'
scriptType: bash
scriptLocation: inlineScript
addSpnToEnvironment: true # Important: This makes the built-in variable `servicePrincipalId` available
inlineScript: |
svcConObjectId=$(az ad sp show --id $servicePrincipalId --query id -o tsv)
echo "##vso[task.setvariable variable=armServicePrincipalId;]$svcConObjectId"
- task: AzureResourceManagerTemplateDeployment@3
displayName: 'Deploy ARM Template (ResourceGroup)'
inputs:
azureResourceManagerConnection: '$'
...
csmFile: '$(Pipeline.Workspace)/CI-Pipeline/$(ciArtifactName)/azuredeploy.base.bicep'
overrideParameters: '-resourceNamePrefix "$(baseResourceNamePrefix)" -resourceNameSuffix "$" -useExistingKeyVault $(keyVaultExists) -servicePrincipalId "$(armServicePrincipalId)"'
Deploy Access Policy (azuredeploy.base.bicep
):
resource keyVaultAccessPoliciesRes 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
parent: keyVaultRes
name: 'add'
properties: {
accessPolicies: union(empty(servicePrincipalId) ? [] : [
{
tenantId: subscription().tenantId
objectId: servicePrincipalId
permissions: keyVaultAppPermissions
}
], [
])
}
}
Read all or selected secrets and provide them as variables (templates.deploy-to-stage.yml
):
# Note: This step bases on the Access Policy created for thepipeline's Service Principal in first deployment job
- task: AzureKeyVault@1
displayName: 'Gather KeyVault values'
inputs:
azureSubscription: '$'
KeyVaultName: '$(keyVaultName)'
SecretsFilter: 'appInsightsConnectionString' # Comma-separated list of secrets -> The're made available as pipeline variables with same name
RunAsPreJob: false # No impact: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-key-vault?view=azure-devops#arguments