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
andapplicationInsightsAppName
both use theappName
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. Theconcat
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 thestorageAccount
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 theapplicationInsights
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.
Previous post
Implement v-model in Vue.jsNext post
Manage Azure Key Vaults using Bicep