Deploy Azure Function Apps with Bicep

Janne Kemppainen |

I’ve been recently working with some twenty plus Azure Function Apps that I need to manage. Previously, those functions had been handcrafted and managed manually from the Azure portal. In this post I will share how I switched to using Bicep templates instead and what I learned in the process.

Bicep is a special infra-as-code language that compiles to an Azure Resource Manager (ARM) template. It reduces the verbosity of ARM templates and generally offers a better user experience. Since it’s directly based on the ARM templates it’s always up to date when the underlying ARM APIs change, which is a nice feature.

The official documentation also contains a Bicep deployment example which you’ve probably already found, too.

The anatomy of a Function App

When you deploy a Function App on Azure it’s not just a single service that contains all that is needed. The Function App resource is just the core but it needs other supporting resources around it. You can see this by exporting the ARM template of one of your functions.

If you want to define your infrastructure as code then you’ll also need to know what supporting components are required. Here’s the essential list of things that are needed, or useful:

  • Function App: This is the application itself. It contains the direct configurations.
  • Hosting plan: Each Function App requires a hosting plan. A hosting plan can be shared between multiple functions, or you can create a separate plan for each function. There are three hosting plan options to choose from. You can read more details from the Azure Functions hosting options documentation.
  • Storage account: It might come as a surprise that a Function App doesn’t include storage for the application code. Instead, it needs a separate storage account to store the application bundle. The same storage account can be shared across functions.
  • Application Insights (optional): You can monitor your application by configuring an Application Insights instance. It collects the logs from your Function App so that you can see when something goes wrong.
  • Deployment slot (optional): A Function App can utilize deployment slots to define additional environments such as staging or development. Slots can also be used to enable zero-downtime deployments. Each slot is a separate instance that runs under its own domain. The Consumption hosting plan allows only one additional slot per Function App.

Now that you have an idea of what’s needed, we can continue to the deployment itself.

Project organization

We can organize our Bicep deployments into modules to reduce repetition. This makes it easier to deploy multiple Function Apps as we only need to customize the module parameters for each deployment.

The deployment directory could look something like this:

.
├── acme-catalog-api.bicep
├── acme-purchases-api.bicep
└── modules
    └── functions.bicep

At the top level, each Function App has its own Bicep file which then references the functions.bicep module. New functions can be added easily by creating additional bicep files.

Functions module

So let’s first create the functions.bicep module. We already know what components a function needs, so we can derive the basic module structure.

Each Function App has to have a globally unique name. Therefore, we must be able to pass that as an input parameter. We’d also like to configure the app settings and tags.

Let’s also assume that all of our functions are based on Node.js. This way we can define the functions runtime and Node version on the module level. Let’s also configure the function to run from a package file.

Here’s the complete module file:

@description('Function App name')
param appName string

@description('Additional Function App settings')
param appSettings array = []

@description('Tags')
param tags object = {}

@description('Resource group name, use default value')
param resourceGroupName string = resourceGroup().name

@description('Resource location')
param location string = resourceGroup().location

@description('Log Analytics workspace name')
param logAnalyticsWorkspaceName string = appName

var functionAppName = appName
var applicationInsightsName = appName
var storageAccountName = '${uniqueString(resourceGroup().id)}azfunctions'
var functionAppSettings = concat(appSettings, [
    {
      name: 'AzureWebJobsStorage'
      value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
    }
    {
      name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
      value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
    }
    {
      name: 'WEBSITE_CONTENTSHARE'
      value: toLower(functionAppName)
    }
    {
      name: 'FUNCTIONS_EXTENSION_VERSION'
      value: '~4'
    }
    {
      name: 'WEBSITE_NODE_DEFAULT_VERSION'
      value: '~18'
    }
    {
      name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
      value: applicationInsights.properties.ConnectionString
    }
    {
      name: 'FUNCTIONS_WORKER_RUNTIME'
      value: 'node'
    }
    {
      name: 'WEBSITE_RUN_FROM_PACKAGE'
      value: '1'
    }
  ])

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  tags: tags
}

resource hostingPlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: 'FunctionApps'
  location: location
  sku: {
    name: 'Y1'
    tier: 'Dynamic'
  }
  tags: tags
}

resource functionApp 'Microsoft.Web/sites@2022-03-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp'
  identity: {
    type: 'SystemAssigned'
  }
  tags: tags
  properties: {
    serverFarmId: hostingPlan.id
    siteConfig: {
      appSettings: functionAppSettings
      ftpsState: 'Disabled'
      minTlsVersion: '1.2'
      http20Enabled: true
    }
    httpsOnly: true
  }
}

resource deploymentSlot 'Microsoft.Web/sites/slots@2022-03-01' = {
  name: '${functionApp.name}/deploy'
  location: location
  tags: tags
  properties: {
    serverFarmId: hostingPlan.id
    siteConfig: {
      appSettings: functionAppSettings
    }
  }
}

resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
  name: logAnalyticsWorkspaceName
  location: location
  tags: tags
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 90
    workspaceCapping: {
      dailyQuotaGb: 1
    }
  }
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Request_Source: 'rest'
    WorkspaceResourceId: logAnalytics.id
  }
  tags: tags
}

First, let’s go through the input parameters which are defined with the param keyword.

  • appName is the name of the Function App. The value must be globally unique and also determines the app URL which is of the form <appName>.azurewebsites.net.
  • appSettings is an array of objects that can be passed to set custom app configurations and environment variables.
  • tags can be used to set resource tags.
  • resourceGroupName uses the current resource group of the deployment by default and doesn’t need to be provided.
  • location is also automatically derived from the resource group location.
  • logAnalyticsWorkspaceName is the name of the log analytics workspace. It defaults to the same name as the function app.

Then we have some variables.

  • functionAppName and applicationInsightsAppName both use the appName parameter but here you could customize these with a common prefix, for example. Just remember that if you change the name of the app here then the URL will also be different.
  • storageAccountName is the name of the storage account that holds the application code. Since storage accounts also need to be globally unique we calculate a hash of the resource group id as part of the name.
  • functionAppSettings contains the application settings and environment variables. The concat function is used here to combine the input parameter settings with other required configurations. You can find a reference of app settings from the Microsoft documentation.
    • AzureWebJobStorage contains the connection string to the storage account. It references the storageAccount resource for the access key.
    • WEBSITE_CONTENTAZUREFILECONNECTIONSTRING contains the same storage account connection string. This setting is required in the Consumption hosting plan that is used in this example.
    • WEBSITE_CONTENTSHARE contains the file path to the function app code in the storage account.
    • FUNCTIONS_EXTENSION_VERSION defines the version of the Functions runtime. Version ~4 is the most recent at the time of this writing.
    • WEBSITE_NODE_DEFAULT_VERSION is used to configure the Node version (on Windows runtimes).
    • APPLICATION_INSIGHTS_CONNECTION_STRING enables logging to Application Insights. It references the connection string from the applicationInsights resource.
    • FUNCTIONS_WORKER_RUNTIME sets Node.js as the worker runtime.
    • WEBSITE_RUN_FROM_PACKAGE enables the function app to run from a zip deployment.

Moving on to the resources, first we have storageAccount which uses the storageAccountName to create a shared storage account for all function apps in the same resource group. The account type is standard locally redundant storage. This resource was referenced in the storage connection strings.

The hostingPlan resource defines where the function apps run. The Y1 Dynamic plan is the consumption plan where billing is based on usage. By default, this runs on Windows but if you specifically need to use Linux you can set kind: 'linux' in the configuration. The name is hardcoded to ‘FunctionApps’ so all functions in the same resource group will use the same plan. You could also create a unique hosting plan for each app if preferred.

The functionApp resource contains the actual function app definition. Here you can add your own configuration options. In this example, FTP deployments are disabled, minimum TLS version is set to 1.2, HTTP/2 is enabled, and all traffic is redirected to the HTTPS protocol. The serverFarmId property selects the hosting plan, so it needs to point to the ID of the hostingPlan resource.

The deploymentSlot resource creates a deployment slot for the function app. The name of the slot must start with the function app name, followed by a /, and the name of the slot. The URL for the slot consists of the function app name, followed by the slot name separated with a -, e.g. <appName>-deploy.azurewebsites.net. The slot can have its own configuration.

In my case, the slot is used for zero-downtime deployments. A new application version can be deployed to the deploy slot, and then the slots can be swapped with the current production deployment which gets rid of the momentary downtime of a normal deployment. The slots can be swapped with the az functionapp deployment slot swap command.

Finally, we have the logAnalytics and applicationInsights resources. The workspace based application insights resource requires a shared log analytics workspace. The workspace name is derived from the function app name as default, but ideally you’d want to use a shared workspace for all your function apps. The application insights resource references the shared workspace using the WorkspaceResourceId property. Similarly, the connection string from this resource was referenced in the function app settings.

You can alter the log analytics configurations to your liking. This configuration uses the pay-as-you-go pricing model with a daily quota of 1 GB and a retention period of 90 days.

App deployment

Now let’s consider an imaginary API endpoint acme-purchases-api that handles transactions for a webshop. We can use the functions.bicep template to easily deploy the function app infrastructure.

The Bicep deployment file could look something like this:

@secure()
param paymentApiSecret string

@description('Resource location')
param location string = resourceGroup().location

var appSettings = [
	{
	    name: 'PAYMENT_API_SECRET'
	    value: paymentApiSecret
	}
	{
		name: 'PAYMENT_API_TOKEN'
		value: 'abcd1234'
	}
]

var tags = {
    Environment: 'prod'
    AutomationScript: 'acme-purchases-api.bicep'
}

module fn 'modules/functions.bicep' = {
  name: '${deployment().name}-functionAppDeploy'
  params: {
    appName: 'acme-purchases-api'
    location: location
    appSettings: appSettings
    tags: tags
  }
}

The deployment file expects a parameter called paymentApiSecret which is also marked secure with a decorator. An intermediate variable appSettings is used to make the configuration a little clearer. It references the secret parameter and defines another non-secret value. These values will eventually end up in the function app’s environment variables to be used by your application. Similarly, the tags variable is used to define some tags for the resources.

The Bicep module section is quite simple. It references the functions.bicep file that we just created and fills in the needed parameters. The name argument gives a name for the function app deployment as it’s technically separate from the main deployment.

Your function app infrastructure is now ready to be deployed:

az deployment group create --resource-group acme-prod --template-file acme-purchases-api.bicep

The command should ask you for the secret parameter, and then it should deploy the resources to the chosen resource group. After a successful deployment you should find the new resources from the Azure Portal.

Now you should be able to publish the actual application code with Azure Functions Core Tools.

Final thoughts

By now, you should have a good starting point for defining Azure functions with Bicep. In the next set of posts, we’ll delve into managing Key Vaults in a similar fashion. Stay tuned as we explore how to use key vault references to effortlessly populate secrets in your function app.

You can continue by reading the next post in the series: Manage Azure Key Vaults using Bicep.

Subscribe to my newsletter

What’s new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy