Azure Bicep - Deploy Function Apps with KeyVault references
Darren Fuller
Posted on December 31, 2021
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 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 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:
- 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 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.
Parameters
For this demo template I want to make 3 things configurable.
- The prefix for all of the resources (but defaulted to "demo")
- The Storage Account SKU (defaulted to Standard LRS)
- The API key for the external service
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.
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.
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.
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.
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.
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.
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.
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)
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.
You can check that permissions have been assigned by looking in the access control section of the KeyVault instance.
And we can see the KeyVault reference in the Azure Function configuration.
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.
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.
Posted on December 31, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.