Host Azure Function on Linux consumption plan

Host Azure Function on Linux consumption plan

The dynamic consumption plan allows you to pay only for the resources you use when your function is running. Thus, if there are no incoming requests and a function does nothing, you don't pay for it.

I will use Bicep to provision all necessary resources, but it is also possible to work with the Azure portal for that purpose.

What we need to start with is to create the storage account. Azure Function requires to have one to store configuration files and we are going to utilize it to store the code of our function.

The following code creates a storage account and the release container.

param appName string
param location string

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: '${appName}storage'
  location: location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_ZRS'
  }
  properties: {
    accessTier: 'Hot'
    minimumTlsVersion: 'TLS1_2'
  }
}

resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
  name: 'default'
  parent: storageAccount
}

resource releasesContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
  name: 'releases'
  parent: blobServices
  properties: {
    publicAccess: 'None'
  }
}

Microsoft suggests using managed identity over connection strings. Therefore, our function will not have any shared keys to access the storage account. Instead, we create a user-assigned managed identity and grant it Blob Data Owner permissions. An identity with such a role has full access to the storage blob container and data required for the normal operation of a function.

var storageBlobDataOwnerRoleId = 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2021-09-30-preview' = {
  name: appName
  location: location
}

resource functionAppStorageBlodDataOwnerUserAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storageAccount.id, storageBlobDataOwnerRoleId, identity.id)
  scope: storageAccount
  properties: {
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataOwnerRoleId)
    principalId: identity.properties.principalId
    principalType: 'ServicePrincipal'
  }
}

The last resource we must provision before creating the function is a server farm with a dynamic plan.

resource hostingPlan 'Microsoft.Web/serverfarms@2022-09-01' = {
  name: appName
  location: location
  sku: {
    name: 'Y1' 
    tier: 'Dynamic'
  }
  properties:{
    reserved: true
  }
}

At this point, all our preparations are done, and we can proceed to create our function.

param packageName string

resource functionApp 'Microsoft.Web/sites@2022-09-01' = {
  name: '${appName}-func'
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}' : { }
    }
  }
  kind: 'functionapp,linux'
  properties: {
    reserved: true
    enabled: true
    serverFarmId: hostingPlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'dotnet'
        }
        {
          name: 'WEBSITE_RUN_FROM_PACKAGE'
          value: '${storageAccount.properties.primaryEndpoints.blob}releases/${packageName}'
        }
        {
          name: 'WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID'
          value: identity.id
        }
        {
          name: 'AzureWebJobsStorage__accountName'
          value: storageAccount.name
        }
        {
          name: 'AzureWebJobsStorage__credential'
          value: 'managedIdentity'
        }
        {
          name: 'AzureWebJobsStorage__clientId'
          value: identity.properties.clientId
        }
      ]
    }
  }
}

The functionApp resource is provisioning the function with the user-assigned managed identity we created earlier. The kind element is set to functionapp,linux, indicating that we would like to use the Linux function.

The most interesting part here is the appSettings block. Let's delve into it.

FUNCTIONS_EXTENSION_VERSION and FUNCTIONS_WORKER_RUNTIME are settings that define the function's version and runtime. I'm using the 4th version of the Azure function, which, at the time of writing, is the latest and recommended version. Regarding the runtime, it can be not only dotnet but also node, java, etc.

WEBSITE_RUN_FROM_PACKAGE specifies the path to the zip file containing the release of a function. As you can see, it points to the storage account and the container we created earlier. The parameter packageName determines the name of the zip file.

WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID needs to be set if we use a managed identity for accessing the storage account. Otherwise, it can be omitted.

The AzureWebJobsStorage settings specify the function storage account and identity required to access it.

The complete Bicep file can be found on GitHub.

Azure Functions Consumption plan hosting

Storage Blob Data Owner role

Storage considerations for Azure Functions

Image credits: Jane__ml