Deploy a Flex Consumption Function App With Terraform
In May 2024 Microsoft released the Public Preview of the new Flex Consumption Azure Functions. This is an exciting release, primarily because it finally allows a consumption Function App to be VNet integrated. Before the release of Flex Consumption Function Apps, we had to either decide to use a consumption plan or allow access to internal resources (and pay 24/7 for the privilege). While there have been workarounds (e.g. run a Function App in Container Apps) none were ideal.
However things aren’t quite as simple as one might expect. Nestled in the release of Flex Consumption Function Apps is a list of site property and application setting deprecations. Until now the same API calls and properties were used to deploy Web Apps, Logic Apps, and Function Apps. This makes some sense as, under the hood, they all run on the same Azure platform. Logic Apps and Function Apps (especially Linux versions) just run a pre-defined container on an App Service Plan. (Ok, I’m probably grossly simplifying the process, and I’m sure the Azure engineers will take exception to this).
Flex Consumption Function Apps are the first deployment to require the use of the functionAppConfig
property in the Microsoft.Web/sites
API. This change requires us to refactor our Flex Consumption deployment code. Unlike other SKUs where changing the Service Plan is all that is required to change the deployment, Flex Consumption functions require a new API call. For now this means that we cannot deploy Flex Consumption Function Apps with the AzureRM provider as the deployment fails (see hashicorp/azurerm#26672) as the upstream API doesn’t support the functionAppConfig
property.
All is not lost. It’s times like this where we can fall back to the AzAPI provider to make things work. Initially, we need to deploy ourselves a Flex Consumption Service Plan, which we can do with the AzureRM provider.
resource "azurerm_resource_group" "flex_function" {
name = "flex-function-rg"
location = "East US"
}
resource "azurerm_service_plan" "flex_function" {
name = "flex-function-plan"
resource_group_name = azurerm_resource_group.flex_function.name
location = azurerm_resource_group.flex_function.location
os_type = "Linux"
sku_name = "FC1"
}
We also need to deploy a Storage Account and a Container for the Function App. This is where the function code is stored and run from, and also where the platform manages the execution for various triggers.
resource "azurerm_storage_account" "flex_function" {
name = "flexfuncsa"
resource_group_name = azurerm_resource_group.flex_function.name
location = azurerm_resource_group.flex_function.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_container" "flex_function" {
name = "my-flex-consumption-app"
storage_account_name = azurerm_storage_account.flex_function.name
container_access_type = "private"
}
Deploying the Function App itself is slightly more difficult as the 2023-12-01
API is not documented in the deployment reference site. But after trawling through the REST API docs, I was able to construct the following AzAPI deployment for a Flex Consumption Function App.
resource "azapi_resource" "flex_function" {
type = "Microsoft.Web/sites@2023-12-01"
name = "my-flex-consumption-app"
location = azurerm_resource_group.flex_function.location
parent_id = azurerm_resource_group.flex_function.id
body = jsonencode({
kind = "functionapp,linux"
properties = {
serverFarmId = azurerm_service_plan.fc.id
httpsOnly = true
functionAppConfig = {
deployment = {
storage = {
type = "blobContainer"
value = "${azurerm_storage_account.flex_function.primary_blob_endpoint}${azurerm_storage_container.flex_function.name}"
authentication = {
type = "SystemAssignedIdentity"
}
}
}
runtime = {
name = "python"
version = "3.11"
}
scaleAndConcurrency = {
alwaysReady = [
{
name = "my-function"
instanceCount = 2
},
{
name = "my-function-group"
instanceCount = 4
},
]
instanceMemoryMB = 2048
maximumInstanceCount = 40
triggers = {}
}
}
siteConfig = {
appSettings = [
{
# Is ignored, but will be added if we don't provide it
name = "FUNCTIONS_EXTENSION_VERSION"
value = "~4"
},
# WebJobs Configuration
{
name = "AzureWebJobsDashboard__accountName"
value = azurerm_storage_account.flex_function.name
},
{
name = "AzureWebJobsStorage__accountName"
value = azurerm_storage_account.flex_function.name
},
{
name = "AzureWebJobsFeatureFlags"
value = "EnableWorkerIndexing"
},
]
}
virtualNetworkSubnetId = data.azurerm_subnet.app_service_delegated.id
}
})
identity {
type = "SystemAssigned"
}
}
# Grant the Function App permissions to the storage account
resource "azurerm_role_assignment" "fa-storage-blob_owner" {
principal_id = azapi_resource.flex_function.identity[0].principal_id
role_definition_name = "Storage Blob Data Owner"
scope = azurerm_storage_account.flex_function.id
}
Let’s step through the above code. Everything is the same for other function apps except for the functionAppConfig
section. In here we configure many of the settings that were previously done either via app settings, or in the siteConfig
section itself. The runtime
property controls the runtime used by the Function App, and replaces the
FUNCTIONS_WORKER_RUNTIME
and FUNCTIONS_WORKER_RUNTIME_VERSION
App Settings. The deployment
property controls where the Function App will store and load the Function code, and replaces the WEBSITE_CONTENTSHARE
and WEBSITE_CONTENTAZUREFILECONNECTIONSTRING
App Settings. It’s nice to have managed identity authentication as a first-class explicit citizen.
The scaleAndConcurrency
property is where things get more interesting. The maximumInstanceCount
property replaces Site Config functionAppScaleLimit
property and the instanceMemoryMB
replaces the containerSize
property. The triggers
setting replaces the FUNCTIONS_MAX_HTTP_CONCURRENCY
App Setting. That is relatively simple. The alwaysReady
property replaces the alwaysOn
property, but does so in a more flexible way. While alwaysOn
was an all-or-nothing flag, the new alwaysReady
property allows for finer-grained control over the exact number of instances running and for which specific functions.
And there you have it. A brand new Flex Consumption Function App deployed with Terraform and ready to load with your actual code.
One thing I did notice working through the code is that the depreciation documentation mentioned a remoteBuild
parameter, which replaces the ENABLE_ORYX_BUILD
and SCM_DO_BUILD_DURING_DEPLOYMENT
app settings. However I was unable to find any reference to remoteBuild
in the code anywhere. This wasn’t an issue in the end, as the Function App Core Tools publish
command defaults to triggering a remote build.