SoliDeoGloria.tech

Technology for the Glory of God

Deploy a Flex Consumption Function App With Terraform

  • 5 minutes

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.