DEV Community

Cover image for Deploy Azure Functions code with Terraform
Max Ivanov
Max Ivanov

Posted on • Originally published at maxivanov.io

Deploy Azure Functions code with Terraform

For a step by step guide on provisioning cloud resources needed to run Azure Functions, check Deploy Azure Functions with Terraform.

This post focuses on how you can publish code to a function app with Terraform. Here, the deployed app is a hello-world Node.js function, but the process is language-agnostic.

Using a package file is the recommended way to run Azure Functions. When new code is uploaded, the switch is atomic (i.e. safe). Performance is better and cold starts are faster. It's easy to rollback to a previous deployment by pointing to the corresponding package.

There are 2 modes of package deployment: url and app service. We will see the benefits and limitations of both as well as how to implement them with Terraform.

Code for Linux/Windows + Consumption/Premium configurations in Github.

Deploy from Package

In a nutshell, deploying from package means taking a package (zip file) and mounting its content to the read-only /home/site/wwwroot directory.

Source package can be stored in a remote storage or be uploaded to the app service, in the /home/data/SitePackages directory.

Deployment mode is dictated by the WEBSITE_RUN_FROM_PACKAGE app setting.

Package in a remote storage

Storage can be anything, as long as the package can be downloaded by the app service at runtime. That highlights a downside of this option - whenever the app is restarted, its zip has to be re-downloaded from the storage.

The most common approach is to host the package in a Blob Storage and generate a SAS URL, granting limited access to the package via the function app configuration:

WEBSITE_RUN_FROM_PACKAGE = URL with SAS
Enter fullscreen mode Exit fullscreen mode

That's another inconvenince of this deployment method:

  • there's now a secret in the app settings to manage (can leak through configuration/state/app code) AND
  • the SAS token may expire (the app won't be able to download the package and will fail to start)

Also documentation states:

When running a function app on Windows, the external URL option yields worse cold-start performance. When deploying your function app to Windows, you should set WEBSITE_RUN_FROM_PACKAGE to 1 and publish with zip deployment.

On a good side, running from a package URL is well-supported by both Linux and Windows environments in both Consumption and Premium plans.

Package in app service

At deployment time, the package is uploaded to the /home/data/SitePackages directory. This directory also has a packagename.txt file which contains nothing but the name of the package that's currently in use. You can update the file manually to point to a different package and the function app will restart shortly and will use that package's code.

To upload the package to app service, you should use the Zip Deployment strategy combined with the app setting:

WEBSITE_RUN_FROM_PACKAGE = 1
Enter fullscreen mode Exit fullscreen mode

Without the app setting, zip deploy will simply extract the package to the /home/site/wwwroot directory loosing all benefits of run-from-package deployment.

Since there's no need to download the package over the network when the function app starts, it should result in faster cold starts.

Note that as of March 2021, WEBSITE_RUN_FROM_PACKAGE = 1 is still not supported in Linux Consumption. Windows hosts and Linux Premium work fine with it though.

Deploy code with Terraform

To keep the post short I assume you have the basic Azure Functions Terraform script ready (as per this post).

Alternatively, refer to this post's companion repository for full configuration example in different environments.

Now we can focus on what it takes to publish/upload the code to Azure.

WEBSITE_RUN_FROM_PACKAGE = <url>

There's a great article by Adrian Hall which describes the process of deploying the code from Azure Storage.

I wanted to revisit the solution to highlight newer azurerm_storage_account_blob_container_sas Terraform resource as well as suggest compressing the code with Terraform without relying on npm. Simpler functions may not even use external modules, so it's good to not have Node.js and NPM as dependencies in the deployment environment.

For both deployment modes, we need to compress the function code for it to be uploaded. For this, we can use the archive_file data type.

data "archive_file" "file_function_app" {
  type        = "zip"
  source_dir  = "../function-app"
  output_path = "function-app.zip"
}
Enter fullscreen mode Exit fullscreen mode

Upload the archive to the Azure Storage Blob:

resource "azurerm_storage_blob" "storage_blob" {
  name = "${filesha256(var.archive_file.output_path)}.zip"
  storage_account_name = azurerm_storage_account.storage_account.name
  storage_container_name = azurerm_storage_container.storage_container.name
  type = "Block"
  source = var.archive_file.output_path
}
Enter fullscreen mode Exit fullscreen mode

Create a read-only SAS for the Blob. Mind the expiration date - past the date the function app scale out/restart operations will fail as the packge link won't work anymore. Using the Service SAS is optimal here, as it provides limited access compared to the Account SAS generated by azurerm_storage_account_sas.

data "azurerm_storage_account_blob_container_sas" "storage_account_blob_container_sas" {
  connection_string = azurerm_storage_account.storage_account.primary_connection_string
  container_name    = azurerm_storage_container.storage_container.name

  start = "2021-01-01T00:00:00Z"
  expiry = "2022-01-01T00:00:00Z"

  permissions {
    read   = true
    add    = false
    create = false
    write  = false
    delete = false
    list   = false
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, provide full package URL in the WEBSITE_RUN_FROM_PACKAGE app setting:

resource "azurerm_function_app" "function_app" {
  name                       = "${var.project}-function-app"
  resource_group_name        = azurerm_resource_group.resource_group.name
  location                   = var.location
  app_service_plan_id        = azurerm_app_service_plan.app_service_plan.id
  app_settings = {
    "WEBSITE_RUN_FROM_PACKAGE"    = "https://${azurerm_storage_account.storage_account.name}.blob.core.windows.net/${azurerm_storage_container.storage_container.name}/${azurerm_storage_blob.storage_blob.name}${data.azurerm_storage_account_blob_container_sas.storage_account_blob_container_sas.sas}",
    "FUNCTIONS_WORKER_RUNTIME" = "node",
    "AzureWebJobsDisableHomepage" = "true",
  }
  os_type = "linux"
  site_config {
    linux_fx_version          = "node|14"
    use_32_bit_worker_process = false
  }
  storage_account_name       = azurerm_storage_account.storage_account.name
  storage_account_access_key = azurerm_storage_account.storage_account.primary_access_key
  version                    = "~3"
}
Enter fullscreen mode Exit fullscreen mode

That's it. Once you run terraform apply, it will prepare the package, upload it to the storage, generate the link and put it to the app setting. The function app will restart and in a few seconds your app will run the new code.

WEBSITE_RUN_FROM_PACKAGE = 1

Prepare the package zip:

data "archive_file" "file_function_app" {
  type        = "zip"
  source_dir  = "../function-app"
  output_path = "function-app.zip"
}
Enter fullscreen mode Exit fullscreen mode

Configure function app using the WEBSITE_RUN_FROM_PACKAGE setting to expect the package in the file system:

resource "azurerm_function_app" "function_app" {
  name                       = "${var.project}-function-app"
  resource_group_name        = azurerm_resource_group.resource_group.name
  location                   = var.location
  app_service_plan_id        = azurerm_app_service_plan.app_service_plan.id
  app_settings = {
    "WEBSITE_RUN_FROM_PACKAGE" = "1",
    "FUNCTIONS_WORKER_RUNTIME" = "node",
    "AzureWebJobsDisableHomepage" = "true",
  }
  os_type = "linux"
  site_config {
    linux_fx_version          = "node|14"
    use_32_bit_worker_process = false
  }
  storage_account_name       = azurerm_storage_account.storage_account.name
  storage_account_access_key = azurerm_storage_account.storage_account.primary_access_key
  version                    = "~3"
}
Enter fullscreen mode Exit fullscreen mode

The code will be pushed to the function app with Azure CLI command:

locals {
    publish_code_command = "az webapp deployment source config-zip --resource-group ${azurerm_resource_group.resource_group.name} --name ${azurerm_function_app.function_app.name} --src ${var.archive_file.output_path}"
}
Enter fullscreen mode Exit fullscreen mode

We use null_resource to run the publish command. Note it will be triggered every time the contents of the package file changes. If there's no change, the code won't be uploaded (makes sense!).

resource "null_resource" "function_app_publish" {
  provisioner "local-exec" {
    command = local.publish_code_command
  }
  depends_on = [local.publish_code_command]
  triggers = {
    input_json = filemd5(var.archive_file.output_path)
    publish_code_command = local.publish_code_command
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see this method is simpler and as we discussed earlier it results in faster cold starts. So if you're not running on Linux Consumption, use this approach.

References

...

Deploy Azure Functions resources as well as code with Terraform when you don't want extra dependencies used just for publishing.

If you like this type of content you can follow me on Twitter for the latest updates.

Top comments (0)