Requirement
To deploy a new version of an Azure Durable Function using Terraform to a Function App with zero downtime
Release Pipeline
The production deployment is triggered when a modification to the main repository branch is committed. The production release pipeline path looks like this:
Plan & Apply Steps include the following tasks:
- Install Terraform
- Initialise Terraform – Command: init
- For Plan => Plan Terraform – Command: plan
- For Apply => Validate and Apply Terraform – Command: validate and apply
Deploy Function Step Tasks:
Task settings:
Terraform Modules
The function app module that is used for all my Azure functions includes the following:
- Application Insights:
resource “azurerm_application_insights” “this” {name = var.application_insights_namelocation = var.locationresource_group_name = var.resource_group_nameworkspace_id = var.log_analytics_workspace_idapplication_type = “web”}
- Function App:
resource “azurerm_function_app” “functionapp” {name = var.function_app_namelocation = var.locationresource_group_name = var.resource_group_nameapp_service_plan_id = var.app_service_plan_idstorage_account_name = var.storage_account_namestorage_account_access_key = var.storage_account_access_keyversion = “~4”enable_builtin_logging = falseapp_settings = merge(var.app_settings,{ “APPLICATIONINSIGHTS_CONNECTION_STRING” = azurerm_application_insights.this.connection_string },{ “APPINSIGHTS_INSTRUMENTATIONKEY” = azurerm_application_insights.this.instrumentation_key },{ “AzureFunctionsWebHost__hostid” = “${substr(var.function_app_name,0,29)}-01″ },{ “WEBSITE_ENABLE_SYNC_UPDATE_SITE” = “true” },{ “WEBSITE_RUN_FROM_PACKAGE” = “1” }identity {type = “SystemAssigned”}site_config {vnet_route_all_enabled = var.enable_vnetalways_on = truehttp2_enabled = true}tags = var.tagslifecycle {ignore_changes = [app_settings[“DurableManagementStorage”],app_settings[“AzureFunctionsWebHost__hostid”],app_settings[“APPLICATIONINSIGHTS_CONNECTION_STRING”],app_settings[“APPINSIGHTS_INSTRUMENTATIONKEY”]]}
}
Notes:
- app_settings are merged from what is passed in from the particular function’s Terraform.
- The function app and the deployment slot need to have unique host ids, therefore AzureFunctionWebHost_hostid setting is used and set dynamically. The Host Id has a maximum length of 32 characters.
- Certain settings should not be overwritten so that they do not cause the “production” app to restart and cause downtime, therefore we use the lifecycle ignore_changes. At the time of writing Terraform does not support “Sticky settings”, here is a link to this functionality, however, I could not work out how to implement it:
So the following configuration settings for the “production” and “stage” slots have to be added manually:
- DurableManagementStorage => “stage” slot and “production” slot pointing to the slot storage accounts (see below)
- AzureFunctionsWebHost__hostid
- APPLICATIONINSIGHTS_CONNECTION_STRING
- APPINSIGHTS_INSTRUMENTATIONKEY
- Deployment Slot:
resource “azurerm_function_app_slot” “functionapp_slot” {name = “stage”location = var.locationresource_group_name = var.resource_group_nameapp_service_plan_id = var.app_service_plan_idfunction_app_name = var.function_app_namestorage_account_name = var.storage_account_namestorage_account_access_key = var.storage_account_access_keyversion = “~4”enable_builtin_logging = falseapp_settings = merge(var.app_settings,{ “APPLICATIONINSIGHTS_CONNECTION_STRING” = azurerm_application_insights.this.connection_string },{ “APPINSIGHTS_INSTRUMENTATIONKEY” = azurerm_application_insights.this.instrumentation_key },{ “AzureFunctionsWebHost__hostid” = “${substr(var.function_app_name,0,29)}-02″ },{ “WEBSITE_ENABLE_SYNC_UPDATE_SITE” = “true” },{ “WEBSITE_RUN_FROM_PACKAGE” = “1” })identity {type = “SystemAssigned”}site_config {vnet_route_all_enabled = var.enable_vnetalways_on = truehttp2_enabled = trueauto_swap_slot_name = “production”}tags = var.tagslifecycle {ignore_changes = [app_settings[“DurableManagementStorage”],app_settings[“AzureFunctionsWebHost__hostid”],app_settings[“APPLICATIONINSIGHTS_CONNECTION_STRING”],app_settings[“APPINSIGHTS_INSTRUMENTATIONKEY”]]}depends_on = [azurerm_function_app.functionapp]}
Notes:
- See the Notes above for the Function App.
- The “auto_swap_slot_name” is set to the “production” slot, therefore once the new version of the function is deployed to the “stage” slot, Azure will wait for the “production” slot to not be active before auto-swapping the slots, therefore no downtime will occur.
- The “depends_on” setting makes sure that the function app is created first before the deployment slot is created.
An example of the Terraform for a durable function
A durable function is required to have separate storage accounts to hold the state, this is so that the “stage” slot and the “production” slot app can be running at the same time and do not collide.
storage_account_name = data.azurerm_storage_account.sacc.name
storage_account_access_key = data.azurerm_storage_account.sacc.primary_access_key
storage_account_connectionstring_slot01 = data.azurerm_storage_account.sacc_slot01.primary_connection_string
storage_account_connectionstring_slot02 = data.azurerm_storage_account.sacc_slot02.primary_connection_string
The corresponding data Terraform:
data “azurerm_resource_group” “sacc_rg” {
name = “ccc-rg-weu-${var.shortenv}-shared-st-01″
}
data “azurerm_storage_account” “sacc” {
name = “cccstweu${var.shortenv}shared01″
resource_group_name = data.azurerm_resource_group.sacc_rg.name
}
data “azurerm_storage_account” “sacc_slot01” {
name = “cccstweu${var.shortenv}slotstate01″
resource_group_name = data.azurerm_resource_group.sacc_rg.name
}
data “azurerm_storage_account” “sacc_slot02” {
name = “cccstweu${var.shortenv}slotstate02″
resource_group_name = data.azurerm_resource_group.sacc_rg.name
}
However, since “sticky keys” do not work or we cannot work out how to implement them, then these storage account connection strings have to be added manually to the “DurableManagementStorage” configuration setting for the stage and production slots. In my current Terraform solution, I do not use the “storage_account_connectionstring_slot01” or the “storage_account_connectionstring_slot02” variables, these are currently just added as placeholders.
After Deployment
Reference 1 below describes adding a Gate evaluation before deployment of the function to the “stage” slot, the idea is that the function should not be deployed to the “stage” slot if there are still executions in flight in the “stage” slot function. Here are my settings for the gate: