Example architecture
Chart below describes a quite typical architecture where Microservice resources and KeyVault have own resource groups. KeyVault is typically located in own resource group because it's often considered as an environment specific resource which is used by multiple resources/services. This blog post covers what I learned when deploying this kind of infrastructure as a code with Bicep when you have to interactive with multiple resource groups.
Bicep implementation
App Service module
This module creates App Service, enables Managed Identity and sets same preferred settings. Module returns principal Id of App Service when resource is created.
@description('Name of the App Service Plan')
param appServicePlanName string
@description('The SKU of App Service Plan.')
param appServicePlanSku string = 'S1'
@allowed([
'Win'
'Linux'
])
@description('Select the OS type to deploy.')
param appServicePlanPlatform string
@description('Name of the App Service')
param appServiceName string
param tags object
param appSettings array
resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
name: appServicePlanName
location: resourceGroup().location
sku: {
name: appServicePlanSku
}
kind: ((appServicePlanPlatform == 'Linux') ? 'linux' : 'windows')
tags:tags
}
resource appService 'Microsoft.Web/sites@2021-02-01' = {
name: appServiceName
location: resourceGroup().location
identity: {
type: 'SystemAssigned'
}
properties:{
serverFarmId: appServicePlan.id
siteConfig:{
alwaysOn: true
ftpsState: 'Disabled'
netFrameworkVersion: 'v6.0'
ipSecurityRestrictions:[
{
ipAddress: 'xxx.xxx.xxx.xxx/32'
action: 'Allow'
tag: 'Default'
priority: 100
name: 'Allow Access from my IP'
}
{
ipAddress: 'Any'
action: 'Deny'
priority: 2147483647
name: 'Deny all'
description: 'Deny all access'
}
]
scmIpSecurityRestrictionsUseMain: true
appSettings: appSettings
}
httpsOnly: true
}
tags:tags
}
output principalId string = appService.identity.principalId
Main Bicep file
Main file orchestrates the creation of infrastructure.
Parameter declaration
@description('Name of the App Service Plan')
param appServicePlanName string
@description('Name of the Application Insights')
param applicationInsightsName string
@description('The SKU of App Service Plan.')
param appServicePlanSku string = 'S1'
@allowed([
'Win'
'Linux'
])
@description('Select the OS type to deploy.')
param appServicePlanPlatform string
@description('Name of the App Service')
param appServiceName string
Utilize App Service Module to create App Service
module appService './appservice.bicep' = {
name: 'appService'
params: {
appServiceName: appServiceName
appServicePlanName: appServicePlanName
appServicePlanPlatform: appServicePlanPlatform
appServicePlanSku: appServicePlanSku
tags:defaultTags
appSettings: appSettings
}
}
Now Managed Identity enabled App Service is created. Next access policies to KeyVault should be created to enable App Service communication with KeyVault.
Failed tryouts
1. Referencing existing KeyVault in Main Bicep file
I first created reference to existing KeyVault like this. With scope I explicitly specified the resource group of KeyVault.
resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
name: keyVaultResourceName
scope: resourceGroup(subscription().subscriptionId, 'rg-keyvault')
}
KeyVault reference seems to be working fine. Access policies of KeyVault should be added next. I found several examples to assign access policies like this:
resource keyVaultPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = {
dependsOn:[
keyVault
]
name: '${keyVault.name}/add'
properties: {
accessPolicies: [
{
objectId: appService.outputs.principalId
permissions: {
secrets: [
'get'
]
}
tenantId: subscription().tenantId
}
]
}
}
Everything looks good so let's deploy infrastructure with the following Azure CLI command
NOTE: 'rg-bicep' is a Resource Group where App Service will be created. KeyVault is not located in that Resource Group!
az deployment group create --resource-group rg-bicep --template-file main.bicep --parameters parameters/dev.parameters.json
This didn't work because the following error occurred:
{
"status": "Failed",
"error": {
"code": "ParentResourceNotFound",
"message": "Can not perform requested operation on nested resource. Parent resource 'MYKEYVAULTRESOURCE' not found."
}
}
Seems that KeyVault is not found. KeyVault is tried to find from the same Resource Group where App Service is located.
NOTE: This is working if KeyVault is located to the same Resource Group
2. Changed scope of Access Policy resource declaration in Main Bicep file
Next I tried to add scope to the Access Policy resource declaration similar way like in KeyVault resource declaration. Visual Studio Code showed immediately warning that this is not allowed: "The root resource scope must match that of the Bicep file. To deploy a resource to a different root scope, use a module".
That warning was a really good because it revealed that this is not the right way to handle this kind of scenario.
Refactored solution
KeyVault policy Module
Handling of KeyVault Access Policy is now separated to the own Bicep Module (keyvaultpolicy.bicep)
@description('Name of the KeyVault resource ex. kv-myservice.')
param keyVaultResourceName string
@description('Principal Id of the Azure resource (Managed Identity).')
param principalId string
@description('Assigned permissions for Principal Id (Managed Identity)')
param keyVaultPermissions object
@allowed([
'add'
'remove'
'replace'
])
@description('Policy action.')
param policyAction string
resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
name: keyVaultResourceName
resource keyVaultPolicies 'accessPolicies' = {
dependsOn:[
keyVault
]
name: policyAction
properties: {
accessPolicies: [
{
objectId: principalId
permissions: keyVaultPermissions
tenantId: subscription().tenantId
}
]
}
}
}
Main Bicep file
Scope configuration of the Module enables that we can configure different resource group for module than which is used in the main Bicep file context.
var keyVaultPermissions = {
secrets: [
'get'
]
}
module keyVault './keyvaultpolicy.bicep' = {
dependsOn: [
appService
]
scope: resourceGroup('rg-keyvault')
name: 'keyVault'
params: {
keyVaultResourceName: keyVaultResourceName
principalId: appService.outputs.principalId
keyVaultPermissions: keyVaultPermissions
policyAction: 'add'
}
}
This solved the problem!