How to deploy a Python Function App in Azure with Terraform

Matt Simmons
Datasparq Technology
4 min readJun 22, 2020

Azure Functions offer a serverless way to execute code. We have a number of components that can be deployed as individual functions. We wanted to be able to manage all the required infrastructure with Terraform, with an aim to deploy the entire project with a single ‘terraform apply’.

The diagram above shows all of the resources required. Most can be Terraformed, but the functions themselves can’t be. Additionally some resources, such as Event Grid Subscriptions, are dependant on the functions existing to deploy, which complicates things.

App Service Plan

To start with, we need an app service plan. The documentation is actually wrong here; the ‘kind’ should be set to ‘FunctionApp’ to get the Linux consumption plan. There’s an issue open on GitHub for this.

resource "azurerm_app_service_plan" "core" {  name                = "${local.app_name}-service-plan"
location = azurerm_resource_group.core.location
resource_group_name = azurerm_resource_group.core.name
kind = "FunctionApp"
reserved = true
sku {
tier = "Dynamic"
size = "Y1"
}
}

Function App

Next we Terraform the function app itself. We specify here that it should be a Python app running on Linux. Note: Function Apps require access to a storage account, so we’ve Terraformed one of those too.

resource "azurerm_function_app" "core" {

name = local.app_name
location = azurerm_resource_group.core.location
resource_group_name = azurerm_resource_group.core.name
app_service_plan_id = azurerm_app_service_plan.core.id
storage_account_name = azurerm_storage_account.function.name
storage_account_access_key = azurerm_storage_account.function.primary_access_key
version = "~3"
os_type = "linux"

site_config {
linux_fx_version = "PYTHON|3.7"
use_32_bit_worker_process = false
}

identity {
type = "SystemAssigned"
}

app_settings = {
FUNCTIONS_WORKER_RUNTIME = "python"
HOUSTON_KEY = file("../keys/houston.key")
}
}

The Functions

There is no way of defining functions as resources, so we’ll use a dummy resource to ensure that the functions are always deployed. The dummy resource uses the ‘null provider’, which doesn’t correspond to any real resource. All it does is run a command to deploy the functions, provided by the ‘local-exec provisioner’.

resource "null_resource" "functions" {

triggers = {
functions = "${local.version}_${join("+", [ for value in local.functions : value["name"] ])}"
}

provisioner "local-exec" {
command = "cd ..; func azure functionapp publish ${local.app_name}; cd terraform"
}
}

The dummy resource’s ‘triggers’ block defines when it should re-run the provisioner. I’ve made this a concatenation of the ‘version’ and ‘functions’ locals that we set in a config file, so that the app will re-deploy any time these change.

# config.yamlname: my-app

version: 0.1.0

functions:
- name: expectation-tester
- name: blob-storage-service
- name: kubernetes-service
- name: cosmos-service

These values get imported into Terraform with the ‘yamldecode’ function:

locals {
app_name = yamldecode(file("../config.yaml"))["name"]
version = yamldecode(file("../config.yaml"))["version"]
functions = yamldecode(file("../config.yaml"))["functions"]
}

Event Grid Topics

We’ve chosen to trigger our functions with Event Grid events. Having a list of function names stored as a local also allows us to easily create event grid topics and subscriptions for each function. We can loop through a list and create multiple resources with ‘count’.

resource "azurerm_eventgrid_topic" "function" {

count = length(local.functions)

name = "topic-${local.functions[count.index]["name"]}"
resource_group_name = azurerm_resource_group.core.name
location = azurerm_resource_group.core.location

depends_on = [
null_resource.functions
]
}

Note: this resource depends on our null resource that deploys the functions. This is important because we won’t be able to create subscriptions on these topics if the functions haven’t been deployed.

Event Grid Subscriptions

Next we create a subscription for each topic each with an Azure function as an endpoint.

Function endpoints for Event Grid subscriptions have only recently been added to the AzureRM Terraform provider (version 2.14.0). The ‘azure_function_endpoint’ block requires a ‘function_id’, but the documentation does not explain how to get a function’s id. I have figured how to write out a function id by importing an existing Event Grid subscription into my state file and reverse engineering it. A function id looks like this:

/subscriptions/<subscription id>/resourceGroups/<resource group name>/Microsoft.Web/sites/<function app name>/functions/<function name>

The subscriptions can then be Terraformed like this (again using count to loop over our list of functions):

resource "azurerm_eventgrid_event_subscription" "function" {

count = length(local.functions)

name = "eg-subscription-${local.functions[count.index]["name"]}"
scope = azurerm_eventgrid_topic.function[count.index].id
event_delivery_schema = "EventGridSchema"

azure_function_endpoint {
function_id = "/subscriptions/${data.azurerm_subscription.primary.id}/resourceGroups/${azurerm_resource_group.core.name}/providers/Microsoft.Web/sites/${azurerm_function_app.core.name}/functions/${local.functions[count.index]["name"]}"
}

depends_on = [null_resource.functions]
}

Additional Benefits

This setup allows us to write a new function and deploy it, along with all of the required infrastructure, without having to update any Terraform configuration. You might have noticed that I also parameterised the name of the app I was deploying, which allows me to re-use this configuration for multiple projects. I simply change the config.yaml and run terraform apply.

--

--