Photo by Felix Mittermeier on Unsplash
Unlocking the Power of Azure Functions Flex Consumption Plan with Pulumi
In this article, we will explore how to provision a Function App in the new Azure Functions hosting plan: the Flex Consumption plan. We will do that using Pulumi and TypeScript.
What is the Azure Functions Flex Consumption plan?
Azure Functions Flex Consumption plan is a new hosting plan for Azure Functions that combines benefits from the Consumption plan and interesting additional features like always-already instances or VNet integration.
You can see a comparison of the different hosting plans for Azure Functions here. And if you want to know more about the Flex Consumption plan, you can check out a nice video about it here.
To be transparent, I was a bit bored by the announcement of "yet another hosting plan for Azure Functions." However, I became enthusiastic when I discovered it solved most of the issues I could have with the other plans.
That's why I decided to see how to deploy a Function App in the Flex Consumption plan using Infrastructure as Code.
The Azure resources to provision
Nothing complicated here. As usual, to provision a Function App, we need to set up:
a Service Plan (with a Flex Consumption SKU)
a Storage account
the Function App itself
In the diagram above, you can see that we will create a Managed Identity for the Function App, a Blob Container to contain the deployment package, and assign the storage blob data contributor role to the Function App managed identity.
Indeed, with Flex Consumption plan, you deploy your application package to a blob container, and your function app runs from this package. That's why you need this blob container. You could enable access for the function app to this container by adding an application setting containing the storage connection string in your function app configuration. However, I prefer using Azure RBAC as it is more secure.
Implement the Infrastructure Code
To create your Pulumi program, you can start from the azure-typescript
template for instance:
pulumi new azure-typescript
This template will already contain the dependency on the Pulumi Azure Native provider that we will use to create our Azure infrastructure.
az functionapp list-flexconsumption-locations -o table
Let's start by creating a resource group:
import * as pulumi from '@pulumi/pulumi'
import * as resources from '@pulumi/azure-native/resources'
const stackName = pulumi.getStack()
const resourceGroup = new resources.ResourceGroup(`rg-flexconsumption-${stackName}`)
You can see that I'm including a prefix corresponding to the resource type and the stack's name in my resource names. I think it's a good convention to follow because it allows you to quickly identify which environment a resource belongs to. However, feel free to adopt any naming convention you like, as long as you have one. Check here if you haven't chosen one yet.
import * as storage from '@pulumi/azure-native/storage'
const storageAccount = new storage.StorageAccount(`stflexconsump${stackName}`, {
resourceGroupName: resourceGroup.name,
allowBlobPublicAccess: false,
kind: storage.Kind.StorageV2,
sku: {
name: storage.SkuName.Standard_LRS,
},
})
Nothing specific for the storage account, just make sure to disable blob public access by setting the allowBlobPublicAccess
to false
.
Now we can create the blob container that will contain the application package we will deploy to the function app. I named it deploymentpackage
but you use any name you like.
const blobContainer = new storage.BlobContainer('deploymentPackageContainer', {
resourceGroupName: resourceGroup.name,
accountName: storageAccount.name,
containerName: 'deploymentpackage',
})
At the time of writing, Flex Consumption is relatively new and only available on the latest versions of the Azure APIs. Since the Azure provider we are using is a "native provider" (generated from Azure APIs to always be up-to-date and cover 100% of the resources in Azure Resource Manager), this is not an issue. However, we need to specify we want the latest version of the API when doing the import from the package to create the Function App resource.
import {AppServicePlan, WebApp} from '@pulumi/azure-native/web/v20231201'
The Application Service Plan is defined using the FlexConsumption
SKU:
const servicePlan = new AppServicePlan(`plan-flexconsumption-${stackName}`, {
resourceGroupName: resourceGroup.name,
sku: {
tier: 'FlexConsumption',
name: 'FC1'
},
reserved: true
})
It's worth noting that currently the Flex Consumption plan is Linux-only so you have to set the reserved
property to true
.
As usual function apps on other hosting plans, the Function App on the Flex Consumption is a WebApp
with the kind
property set to functionapp
.
const config = new Config()
const functionApp = new WebApp(`func-flexconsumption-${stackName}`, {
resourceGroupName: resourceGroup.name,
kind: 'functionapp,linux',
serverFarmId: servicePlan.id,
identity: {
type: 'SystemAssigned'
},
siteConfig: {
appSettings: [
{
name: 'AzureWebJobsStorage__accountName',
value: storageAccount.name
}
]
},
functionAppConfig: {
deployment: {
storage: {
type: 'blobContainer',
value: pulumi.interpolate`${storageAccount.primaryEndpoints.blob}${blobContainer.name}`,
authentication: {
type: 'SystemAssignedIdentity'
}
}
},
scaleAndConcurrency: {
instanceMemoryMB: 2048,
maximumInstanceCount: 100,
},
runtime: {
name: config.require('functionAppRuntime'),
version: config.require('functionAppVersion'),
}
}
});
What changes with the Flex Consumption plan is the introduction of a brand-new section functionAppConfig
on the resource. There are 3 subsections:
deployment
: to define the blob storage container where the application package will be published and the authentication method to use to access it (here the function app managed identity will be used)scaleAndConcurrency
: to define the memory size of the instances, the maximum number of instances and also the always-ready instances if you need some (I haven't set any here)runtime
: to define the language and version runtime (in my example, I set them with values coming from the stack configuration)
The only missing part is to assign the Storage Blob Data Contributor
role on the managed identity of the function app so that it can access the blob container.
What I like to do is to put the Azure Built-In roles needed in the infrastructure code in a specific azureBuiltInRoles.ts
file like this:
export const azureBuiltInRoles = {
contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c',
storageBlobDataOwner: '/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b',
storageBlobDataContributor: '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe'
};
And, then use the role I need in the RoleAssignment
resource.
new RoleAssignment('storageBlobDataContributor', {
roleDefinitionId: azureBuiltInRoles.storageBlobDataContributor,
scope: storageAccount.id,
principalId: functionApp.identity.apply(p => p!.principalId),
principalType: 'ServicePrincipal'
})
We can also create some stack outputs to retrieve interesting information from the created resources like the function app name and the default function app key (just make sure to make it a secret so that's it properly encrypted in the state).
export const functionAppName = functionApp.name
export const defaultFunctionAppKey = pulumi.secret(listWebAppHostKeysOutput({ name: functionApp.name, resourceGroupName: resourceGroup.name })
.functionKeys?.apply(x => x?.default))
We can now just run the pulumi up
command to provision our infrastructure.
Deploy an Azure Function to the new Function App
To ensure the Function App works properly, let's create a new Azure Function and deploy it to our new resource. We can take the .NET 8 isolated process template for instance with a basic HttpTrigger
function.
public class HelloFlexConsumptionPlan
{
private readonly ILogger<HelloFlexConsumptionPlan> _logger;
public HelloFlexConsumptionPlan(ILogger<HelloFlexConsumptionPlan> logger)
{
_logger = logger;
}
[Function("HelloFlexConsumptionPlan")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
return new OkObjectResult("Hello Azure Functions Flex Consumption Plan!");
}
}
Remember that our function app runtime is set with values coming from the stack configuration? So we will have to ensure the configuration is property set for our .NET 8 isolated Azure Function to work properly.
config:
azure-native:location: northeurope
flexconsumption:functionAppRuntime: dotnet-isolated
flexconsumption:functionAppVersion: "8.0"
We can use the Azure Functions Core Tools (make sure you have an up-to-date version) to deploy the Azure Function App:
func azure functionapp publish "func-flexconsumption-dev*****"
func-flexconsumption-dev*
by the functionAppName
stack output value.You can add an other output in your Pulumi program to have the endpoint to use to run the function you have just deployed:
export const functionEndpoint = pulumi.interpolate`https://${functionApp.defaultHostName}/api/HelloFlexConsumptionPlan?code=${defaultFunctionAppKey}`
And then just make a get request on the function endpoint:
Summary
The Azure Functions Flex Consumption plan offers a flexible and powerful hosting option that addresses many limitations of other plans.
Using Pulumi, you can easily provision a Function App on the Flex Consumption plan and start leveraging fast scaling and features like always-ready instances for your Azure Functions. You can find the complete source code used for this article in this GitHub repository.
If you are using other IaC solutions (Bicep or Terraform/OpenTofu with the Az API provider), you can check this sample repository