App Service, ARM, Azure, Bicep, Professional

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:

Screenshot illustrated Role Assignment to the Azure Key Vault

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:

App Service Configuration Screenshot illustrating connectivity to Key Vault

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

Screenshot of output of what-if command on Bicep Modules

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:

  1. App Service Plan
  2. App Service (Create MSI)
  3. Key Vault (Assign MSI Access)
  4. 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.