Chicken and the Egg: How to Handle Bicep Interdependencies
Introduction
That’s title is a mouthful: Chicken and the Egg: How to Handle Bicep Interdependencies. What this means is how to handle deployments when resources depend on each other. In a fairly standard architecture pattern this could be a problem.
What I mean by that is the scenario where a App Service has secrets which are stored in an Azure Key Vault. This makes the Web App require the Key Vault to be present at deployment time. What about accessing the Key Vault? If we want to be most secure, we should use Managed Identities and RBAC permissions.
The conundrum the Managed Identity should exist to create the Key Vault….at least it would make it a lot easier since the Role Assignment is optimized to leverage scope with resource(s) defined in the same file/module. So that means my options are to either not leverage modules or come up with Role Assignment or populate the app settings after the initial deployment. Another option I supposed is to independently manage and deploy the resources; however, that gets messy to keep track of.
What if we could do this in Bicep with modules? It’s possible if you can combine the knowledge of Bicep with how Azure Resource Manager (ARM) deployments work.
PreReqs
- Any “productionalized” version of Bicep (>v0.3)
- Ability to assign RBAC permissions (ours is CI/CD w/ a Service Principle that has such access)
Project Layout
For this I want to keep it simple and clean. So this will mean:
- main.bicep
- modules
- app-service-plan.module.bicep
- app-service-settings.module.bicep
- app-service.module.bicep
- key-vault.module.bicep
I find it easier to start with the modules and work our way back into the main.bicep file. Let’s start with app-service-plan.module.bicep
This will be the first one created.
app-service-plan.module.bicep
param appServiceName string
param tags object
param location string = resourceGroup().location
param appServicePlanTier string
resource appServicePlan 'Microsoft.Web/serverfarms@2021-03-01' = {
name: appServiceName
location: location
tags: tags
kind: 'web'
sku: {
name: appServicePlanTier
}
}
output appServiceId string = appServicePlan.id
This is nothing extraordinary about this template. The biggest thing is the output. If you aren’t familiar with Bicep outputs, please read up on them before going further as understanding them is key (no pun intended)
app-service.module.bicep
Now that the App Service Plan has been created let’s create the necessary App Service and be sure to turn on our Managed Identity
param appName string
param appServicePlanID string
param tags object
param location string = resourceGroup().location
resource webApp 'Microsoft.Web/sites@2018-11-01' = {
name: appName
tags: tags
location: location
kind: 'web'
properties: {
httpsOnly: true
serverFarmId: appServicePlanID
clientAffinityEnabled: false
}
identity: {
type: 'SystemAssigned'
}
}
output webAppMSI string = webApp.identity.principalId
output appServiceName string = webApp.name
In addition to the MSI principal we will need the name. Rather than referencing the version we are passing in we will need the output from this module to help Bicep keep track of it’s dependencies.
Notice here we are not doing anything about the app settings as this will be handled later since it is technically another API segment, as such we can deploy it later as a stand alone.
key-vault.module.bicep
This module will do a couple of things. For different customers I have broken this out by splitting the RBAC assignment and secret populations using a similar technique we are discussing for app settings. However; to keep this simple all the key vault related activities will be contained in this module. If you are interested in this check out Nested Loops in Azure Bicep and How to Securely Populate Key Vault Secrets via Azure DevOps
Also, I have chosen to assign the Secret Officer role as well as setting a variable to note service principal type.
param location string = resourceGroup().location
param tags object
param tenantId string = subscription().tenantId
param keyVaultName string
param principalID string
var roleID= 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'
var principalType = 'ServicePrincipal'
resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {
name: keyVaultName
location: location
tags: tags
properties: {
enabledForTemplateDeployment: true
enablePurgeProtection: true
enableRbacAuthorization: true
enableSoftDelete: true
sku: {
family: 'A'
name: 'standard'
}
softDeleteRetentionInDays: 30
tenantId: tenantId
}
}
resource keyVaultSecrets 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
parent:keyVault
name: 'SecretName'
properties:{
attributes:{
enabled:true
}
value: 'iTs@S3Cr3T!'
}
}
resource roleAssignment'Microsoft.Authorization/roleAssignments@2020-08-01-preview' ={
name: guid(principalID, roleID,keyVaultName)
scope: keyVault
properties: {
roleDefinitionId: '/providers/Microsoft.Authorization/roleDefinitions/${roleID}'
principalId: principalID
principalType: principalType
}
}
output keyVaultSecretURI string = keyVaultSecrets.properties.secretUri
And here is the proof of the assignment of the role to the MSI:
app-service-settings.module.bicep
param appName string
param keyVaultSecretURI string
resource webSiteConnectionStrings 'Microsoft.Web/sites/config@2021-03-01' = {
name: '${appName}/appsettings'
properties: {
'KeyVaultSecret' : '@Microsoft.KeyVault(SecretUri=${keyVaultSecretURI})'
}
}
Small and easy. No joke. Realistically if you aren’t away the sub segments of ARM templates can be either nested in the parent resource or deployed as a standalone. The biggest thing is the name of the sub resource. For those unfamiliar if we didn’t have the name correct, we get the dreaded .....incorrect segment lengths. A nested resource type must have identical number of segments as its resource name.
Now to declare this as a Key Vault reference we need the @Microsoft.KeyVault
keywords. Now how can the App Service access the Key Vault? Thanks to our role assignments in the key-vault.module.bicep
file we already passed in the MSI for our app service so it has been provisioned.
And the proof of the setting being deployed and the App Service Recognizing it:
main.bicep
param defaultLocation string
param env string
param appName string
param appServicePlanTier string
param locationLookup object = {
'centralus': 'cus'
'eastus': 'eus'
'westus': 'wus'
}
param defaultTags object = {
'env' : env
'app-name' : appName
}
var nameSuffix = '${appName}-${env}-${locationLookup[defaultLocation]}'
var functionName = 'app-${nameSuffix}'
var appServiceName = 'appsvc-${nameSuffix}'
var keyVaultName = 'kv-${nameSuffix}'
module keyVaultModule 'modules/key-vault.module.bicep'={
name: 'keyVaultModule'
params: {
keyVaultName: keyVaultName
principalID: appServiceModule.outputs.webAppMSI
location: defaultLocation
tags:defaultTags
}
}
module appServicePlanModule 'modules/app-service-plan.module.bicep' = {
name: 'appServicePlanModule'
params: {
appServiceName: appServiceName
tags: defaultTags
location: defaultLocation
appServicePlanTier: appServicePlanTier
}
}
module appServiceModule 'modules/app-service.module.bicep' = {
name: 'appServiceModule'
params: {
appName: functionName
appServicePlanID: appServicePlanModule.outputs.appServiceId
tags: defaultTags
location: defaultLocation
}
}
module appServiceSettingsModule 'modules/app-service-settings.module.bicep' = {
name: 'appServiceSettingsModule'
params:{
appName: appServiceModule.outputs.appServiceName
keyVaultSecretURI: keyVaultModule.outputs.keyVaultSecretURI
}
}
This main.bicep
file just pulls everything together. There are a few tips and tricks in there on how I break down and assign variables and parameters for those that are interested.
dev.cus.parameters.json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"env": {
"value": "dev"
},
"appName":{
"value": "appsettings"
},
"defaultLocation": {
"value": "centralus"
},
"appServicePlanTier":{
"value": "F1"
}
}
}
Nothing really here it’s just the parameter file.
Deployment Script
az deployment group create --template-file 'main.bicep' --parameters 'parameters/dev.cus.parameters.json' --resource-group 'rg-appservicesettings-dev-cus'
Disclaimer
One thing worth noting if breaking modules apart this way is the impact of using what-if
for your CI/CD Deployments. If unfamiliar I have a post on CI/CD for Bicep deployments.
The issue is since the MSI ID isn’t known until post deployment and Bicep is decompiling to nested ARM Templates this information won’t be available for the PowerShell what-if
. This leads to all the resources depending on the App Service MSI detecting “Ignore”. Can see this below
To read more on this feel free to check out the GitHub discussion
Conclusion
If following along the key thing to take away here is understanding the order in which pieces need to be deployed. By leveraging the sub resources, we are able to address the chicken and the egg dilemma and illustrate how to Handle Bicep interdependencies.
The deployments will occur in the following order:
- App Service Plan
- App Service (Create MSI)
- Key Vault (Assign MSI Access)
- App Service Settings (Add Key Vault Reference)
By deploying in this manner, we are able to consistently deploy applications with no error or dependent permissions to be configured before deploying.