DevOps·

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

Diagram illustrating a resource group setup in Azure. A storage account is linked to a blob container via deployment package, shown by arrows. Another arrow represents the storage blob data contributor role granted to managed identity, connected to the Function App. The Function App is associated with the Application Service plan (Flex Consumption).

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
To keep things simple and focused on the topic, I will not configure any monitoring on this Function App. This means we won't provision the Application Insights and Log Analytics Workspace resources.

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.

When you create your project, you will be asked to choose a default azure location for your stack. Choose a region where Flex Consumption plan is available. You can check that with the following azure cli command 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'
If you have worked with ARM templates, Bicep, or the Terraform AzAPI provider, you should be familiar with the concept of resource API versions. With the Pulumi Azure Native provider, there is also a default API version. This means you don't have to specify a version each time you create a resource. You only need to do this when you want a specific version (newer or older than the default one), as in this case.

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)
You can get additional information to provision a function app on the Flex Consumption plan in Microsoft's documentation. The properties on the Azure resources are the same in Bicep/ARM templates as in Pulumi Azure Native provider, so it's not complicated to follow the documentation.

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:

azureBuiltInRoles.ts
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.

Terminal output showing a Pulumi stack creation. Seven resources including ResourceGroup, AppServicePlan, StorageAccount, BlobContainer, WebApp, and RoleAssignment were created. Status for each resource is shown with creation times. Outputs include secrets for defaultFunctionAppKey and functionEndpoint, and the functionAppName "func-flexconsumption-devad034cfd". Total duration: 1m13s.

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.

HelloFlexConsumptionPlan.cs
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.

Pulumi.dev.yaml
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*****"
Replace func-flexconsumption-dev***** by the functionAppName stack output value.

You can add another 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:

A command line interface showing a highlighted command and the output: "http get (pulumi stack output functionEndpoint --show-secrets)". The output message reads: "Hello Azure Functions Flex Consumption Plan!"

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


The opinions expressed herein are my own and do not represent those of my employer or any other third-party views in any way.

Copyright © 2024 Alexandre Nédélec. All rights reserved.