Azure Bicep - Deploy Function Apps with KeyVault references
Tags: azure, bicep, infrastructure
Published: 2021-12-31 15:55:00

One of the things you often yourself doing when starting a new project, module, or component, is having to define the infrastructure that it is going to run on. In the world of Azure often what you find is that many will just drop into the portal and create the items that they need directly, this obviously isn't good as a repeatable pattern but works for throw-away tests. Others might be braver and fire up the console to use the [Azure CLI](https://docs.microsoft.com/cli/azure/) tool, much better for repeating steps if you remember to script it.

Better yet are those who take that little extra time and generate the environment using templates. Repeatable, consistent, and they give the option of parametrising them so that you can change the scale of components as they're deployed to different environments.

What I want to do in this post is show doing exactly this, by deploying the following components, but also by setting up managed identities, adding RBAC permissions in, and hooking them up all from the same template. By putting in that little bit extra you can go from zero to fully configured platform in minutes, in a consistent and repeatable way.

I'm going to do this using [Bicep](https://docs.microsoft.com/azure/azure-resource-manager/bicep) which is Microsoft's DSL for deploying resources to Azure. Don't be fooled by the 0.4 version number, this is a more than capable language which should take centre stage of your deployment scripts. Unlike ARM templates before it also supports modules, allowing you to break apart your templates without the need for a public end-point to bring them all back together again. But for this post we're only going to need a single template.

## The infrastructure

So what are we deploying? Well for this post the core of the platform is going to be the Azure Function app, but it's going to need some support. We're also going to assume that the platform is going to collect data from an external service, so the secret we want will be an API key passed in to the template. So the full platform will be:

![Overview of Infrastructure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n1hk8r5en2zgtum99k0x.png)

* Azure Function App with System Assigned managed identity and app settings for:
  * API Key from KeyVault using KeyVault references
  * Storage account name
  * Container name
  * Key from Application Insights
* Azure Storage Account with container for future use
  * Also with data contributor permissions assigned to the Function App at the storage account level
* Azure KeyVault
  * Deployed using RBAC for authentication instead of using Access Control Lists
  * With secret for the API key
  * Secret User permissions assigned to the Function App
* Application Insights
* Azure Log Analytics to back the Application Insights instance

And whilst we're at it lets make sure we add some basic security steps like HTTPS only traffic and setting TLS 1.2 as the minimum permitted TLS version.

## Creating the template

The best way to get started with Azure Bicep is to use [Visual Studio Code](https://code.visualstudio.com) with the Bicep extension which includes language server support and some other nice features. But the language server support giving us highlighting, template outlines, auto-complete etc... is the real nice to have feature.

The template is available in full on [Github](https://gist.github.com/dazfuller/7a61bf3dbf7be1b977c9fa54e7cdf17d).

### Parameters

For this demo template I want to make 3 things configurable.

1. The prefix for all of the resources (but defaulted to "demo")
1. The Storage Account SKU (defaulted to Standard LRS)
1. The API key for the external service

![Template parameters](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kc36ilt7d65ge4bz8g1p.png)

In this we have defined the prefix as a string and restrict it's length to a minimum of 3 and a maximum of 6 and default it to "demo". We give the storage account SKU a set of values it needs to be restricted to and default it to "Standard_LRS". Finally we define the API key as a string, but annotate the template to make it a secure parameter. Making this a secure parameter means that we're not going to accidentally leak the value to anyone who can see the template deployment history.

### Variable

Next up are the variables. The bulk of these are defining things like locations and names. I'm using a naming convention of `<prefix>-<service name>-<unique id>`. This lets multiple instances be deployed without (hopefully) hitting any naming conflict issues. The only variation in this is the storage account name as we can't use hyphens, only alpha-numeric characters.

![Template variables](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/in45hrw9u8p6yg3issui.png)
 
The `storageConnectionString` is used later on in the template and is only defined here so I don't repeat myself later.

The `storageBlobDataContributorRole` and `keyVaultSecretsUserRole` are the role definition values for the built in `Storage Blob Data Contributor` and `Key Vault Secrets User` built-in roles which you can find in the [Azure documentation](https://docs.microsoft.com/azure/role-based-access-control/built-in-roles).

### Resources

Right, on to the fun stuff. We've got our parameters and variables, now it's time to put them to good use.

#### Storage account

Azure Storage is typically the beating heart of any platform, and so it's normally the first resource I start with.

```bicep
resource storage 'Microsoft.Storage/storageAccounts@2021-06-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: storageSkuName
  }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
    encryption: {
      keySource: 'Microsoft.Storage'
      services: {
        blob: {
          enabled: true
        }
        file: {
          enabled: true
        }
        queue: {
          enabled: true
        }
        table: {
          enabled: true
        }
      }
    }
  }
  
  resource blobService 'blobServices' = {
    name: 'default'

    resource content 'containers' = {
      name: 'content'
    }
  }
}
```

Out of the box here we're setting the minimum TLS version to 1.2 and enabling HTTPS only traffic. There really should be very very few reasons as to why these are not in every template we produce. We're also turning on encryption of data in all storage services by default.

We have a couple of nested resources which are how we create our containers. In this case I'm creating a container called "content".

#### Log Analytics and Application Insights

Next up we want to deploy an Application Insights instance which will be used by our Azure Function app. It's a couple of years away at the time of writing, but Microsoft will be requiring that all instances are backed by a Log Analytics workspace. This makes a huge amount of sense really as it gives a single place to monitor your application and generate alerts based on health conditions.

```bicep
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
  name: logAnalyticsName
  location: location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
    features: {
      enableLogAccessUsingOnlyResourcePermissions: true
    }
    workspaceCapping: {
      dailyQuotaGb: 1
    }
    publicNetworkAccessForIngestion: 'Enabled'
    publicNetworkAccessForQuery: 'Enabled'
  }
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    publicNetworkAccessForIngestion: 'Enabled'
    publicNetworkAccessForQuery: 'Enabled'
    WorkspaceResourceId: logAnalytics.id
  }
}
```

If you're familiar with ARM templates you might be noticing the lack of `dependsOn` entries. This is because in Bicep, where we use a resource reference to get a value (e.g. `logAnalytics.id`) this adds an automatic dependency in for us. The use of the `id` at this point sets the workspace resource instance backing the Application Insights instance. This is a pretty simple setup of the services and I won't dive in any further at this point.

#### KeyVault

Okay, next up, KeyVault. This is core to accessing secrets on the platform in a secure manner. One of the things we're going to do here is enable the RBAC permissions instead of using Access Control Lists (ACLs). Normally when using ACLs we would give dependent services Get and List permissions over Secrets, with RBAC we can instead use the `Key Vault Secret User` built-in role. This makes the management of secrets a lot easier and means we can view permissions in a single place.

```bicep
resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = {
  name: keyVaultName
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: subscription().tenantId
    enableRbacAuthorization: true
    enabledForDeployment: false
    enabledForDiskEncryption: true
    enabledForTemplateDeployment: false
  }

  resource storageNameSecret 'secrets' = {
    name: 'ExternalServiceApiKey'
    properties: {
      contentType: 'text/plain'
      value: apiKey
    }
  }
}
```

The `enableRbacAuthorization` setting is key for us to enable RBAC permissions. Otherwise we're setting up a fairly standard instance. We're also create a new secret as a nested resource using our _secure_ API key parameter. Maybe pick a better secret name than I have, but it works for a demo.

#### Function app

Last up for resources is the Azure Function application.

```bicep
resource plan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: appServicePlanName
  location: location
  kind: 'functionapp'
  sku: {
    name: 'Y1'
  }
  properties: {
  }
}

resource funcApp 'Microsoft.Web/sites@2021-02-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    httpsOnly: true
    serverFarmId: plan.id
    siteConfig: {
      ftpsState: 'Disabled'
      minTlsVersion: '1.2'
      netFrameworkVersion: 'v6.0'
      appSettings: [
        {
          name: 'AzureWebJobsStorage'
          value: storageConnectionString
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: storageConnectionString
        }
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: appInsights.properties.InstrumentationKey
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'dotnet'
        }
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'ApiKey'
          value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::storageNameSecret.name})'
        }
        {
          name: 'ContentStorageAccount'
          value: storage.name
        }
        {
          name: 'ContentContainer'
          value: storage::blobService::content.name
        }
      ]
    }
  }
}
```

This is deployed under a consumption plan (mostly to keep the template a bit simpler), but it's the `appSettings` where things start to get more interesting.

In here we're creating the `AzureWebJobsStorage` and `WEBSITE_CONTENTAZUREFILECONNECTIONSTRING` settings using the connection string we defined earlier in the variables section.

The `APPINSIGHTS_INSTRUMENTATIONKEY` setting is being set using the instrumentation key of the yet-to-be-deployed Application Insights resource which we're accessing just using dot notation.

For `ApiKey` we're creating our KeyVault reference. We could do this using the URL format, but I'm using this format as I think it's easier to read. I'm also not using the secret version as I want to get the latest key each time. Because the secret itself is a nest resource we have to use the double-semi-colon notation `::` to access it's values.

#### Setting up permissions

Having the KeyVault reference is great, but if we deploy this now it will fail. This is because the Function App doesn't have permissions to read secrets, so lets set that up as well.

```bicep
resource storageFunctionAppPermissions 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(storage.id, funcApp.name, storageBlobDataContributorRole)
  scope: storage
  properties: {
    principalId: funcApp.identity.principalId
    principalType: 'ServicePrincipal'
    roleDefinitionId: storageBlobDataContributorRole
  }
}

resource kvFunctionAppPermissions 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(kv.id, funcApp.name, keyVaultSecretsUserRole)
  scope: kv
  properties: {
    principalId: funcApp.identity.principalId
    principalType: 'ServicePrincipal'
    roleDefinitionId: keyVaultSecretsUserRole
  }
}
```

There's a lot happening here, so lets break it down a bit.

We have 2 `roleAssignments` resources here. The first is assigning permissions to the storage account so that later on (if we want) the function app can read and write data to the storage account. This isn't needed for our example, but I added it for demonstration purposes. The second role assignment assigns secret user permissions to the Azure Function app.

Inside of both we're creating a name by generating a guid. This has to be something which is known at deployment time so we can't use values like the principal id as that's only known after the resource deployment. Which values you provide is largely up to you, but I like the format of `target/source/role` as it makes sense when reading it back later.

In the properties we have to say which principal needs access, what role we're giving it, and that it's type is `ServicePrincipal` (for our managed identities). This last point is important, the deployment will work without it but might occasionally throw a `PrincipalNotFound` error as the identity might not have been created before we try assigning permissions. By adding this property we should be waiting for the identity to be created first.

## Deploying

This is the easy part of the process. Assuming you've installed the Azure CLI and have bicep installed you can simply run the following (changing the location and resource group name to suit you)

```bash
az group create --name func-app-demo --location northeurope

az deployment group create --resource-group func-app-demo --template-file function-app.bicep --parameters apiKey=abc123
```

The above command skips the storage account SKU and resource prefix so the defaults are used.

![Deployed resources](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/daq7q3scdqhcavyxn35a.png)

You can check that permissions have been assigned by looking in the access control section of the KeyVault instance.

![Assigned permissions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/13bp8p7jo1vds4z2uru4.png)

And we can see the KeyVault reference in the Azure Function configuration.

![KeyVault reference](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eu6ycpgv2xjemif4gqzg.png)

And that's it. We can now deploy our functions and make use of the app settings already created. And when we're ready we can take this template and deploy out to all of our other environments.

If you get intrigued you can always see what the ARM equivalent of the template might have looked like by running the following.

```bash
az bicep build -f function-app.bicep
```

This will generate a `function-app.json` ARM template file in which you will see of the dependencies added in, and what the shortcuts like `listKeys()` have done for us. I'm sure you'll agree that the bicep is much nicer to read and maintain.