Azure Functions is a powerful platform that simplifies the development of event-driven systems in the cloud. Yet, as many of us have discovered, things can go awry when you least expect it.

When I set out to deploy a .NET 8.0 Azure Function on a Linux-based Azure App Service Plan, everything seemed to be in place — until I logged into the Azure Portal and discovered my Functions were nowhere to be found.

In this article, I will describe my experience wrestling with these “invisible” Azure Functions, explain what caused them to remain hidden, and share insights on how to prevent such mishaps in the future.

Ultimately, I hope this serves as a call to action for improved error handling and more transparent messages during deployment package validation, so that we can all spend more time coding and less time scratching our heads.

In my book, Mastering Terraform, I covered Azure Functions in Chapter 12. At the time, I was working with .NET 6.0 and using a Windows App Service Plan. Recently, at work, I have been building a Microservices based architecture for the backend of an internal application and we had the need of supporting multiple languages — some services were being developed in C# .NET and others were being developed in Python. It’s a whole new Microsoft.

The Service Plan

Anyways, we decided we should use a Linux-based App Service Plan to host our functions. So I went ahead and set one up using Terraform.

resource "azurerm_service_plan" "main" {
  name                = "asp-${var.application_name}-${var.environment_name}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  os_type             = "Linux"
  sku_name            = "EP2"
}

The os_type is the key attribute here. The value of this flag has to correlate with the resource type that you use to provision Function Apps to this Service Plan. As you might be aware, just like other Microsoft.Compute services, the Azure Function has Windows and Linux variants of the “Function App” resource type in the azurerm Terraform provider. The Function App

Below is how I setup the azurerm_linux_function_app to host a .NET 8.0 Azure Function. I am using the .NET Isolated worker runtime. I’m also enabling System Assigned Identity in addition to setting up my own User-Assigned Managed Identity.

resource "azurerm_linux_function_app" "main" {
  name                = "func-${var.application_name}-${var.environment_name}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location

  key_vault_reference_identity_id                = azurerm_user_assigned_identity.function.id
  storage_account_name                           = azurerm_storage_account.function.name
  service_plan_id                                = data.azurerm_service_plan.main.id
  ftp_publish_basic_authentication_enabled       = false
  webdeploy_publish_basic_authentication_enabled = false
  https_only                                     = true

  identity {
    type = "SystemAssigned, UserAssigned"
    identity_ids = [
      azurerm_user_assigned_identity.function.id
    ]
  }

  site_config {
    use_32_bit_worker = false

    application_stack {
      dotnet_version = "8.0"
    }
    cors {
      allowed_origins     = ["https://portal.azure.com"]
      support_credentials = true
    }
    application_insights_connection_string = "@Microsoft.KeyVault(VaultName=${data.azurerm_key_vault.main.name};SecretName=ApplicationInsights-ConnectionString)"
  }

  app_settings = {
    "FUNCTIONS_WORKER_RUNTIME"               = "dotnet-isolated"
    "WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED" = 1
    "WEBSITE_RUN_FROM_PACKAGE"               = 1
    "SCM_DO_BUILD_DURING_DEPLOYMENT"         = "false"
    "FUNCTION_MANAGED_IDENTITY"             = azurerm_user_assigned_identity.function.client_id
  }

}

Worker Runtime

The key configuration settings that configure the .NET Isolated worker runtime are:

  • Application Stack’s .NET Version needs to be 8.0. Due note that the Windows equivalent of the Terraform resource have a similar structure but use a v8.0 value. Too close for comfort if you ask me!
  • App Setting FUNCTIONS_WORKER_RUNTIME set to dotnet-isolated.

According to the official documentation on Azure Functions App Settings, the WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED attribute helps with cold start optimization.

Indicates whether to use a specific cold start optimization when running .NET isolated worker process functions on the Consumption plan. Set to 0 to disable the cold-start optimization on the Consumption plan.

Deployment Context

There are a couple settings that dictate the way you want to deploy your application code:

  • App Setting WEBSITE_RUN_FROM_PACKAGE set to 1 . Essentially says, that we have a zip archive that contains the contents of our deployment rather than a bunch of loose files that will be stored in the wwwroot.
  • App Setting SCM_DO_BUILD_DURING_DEPLOYMENT set to false . Turns off the Azure Functions Site Control Manager’s ability to build code that it pulls directly from source control.

You have to be careful because Linux is a bit of an odd ball. The official documentation calls this out but it goes against what you would expect. If you are using a “Consumption” Service Plan you cannot use WEBSITE_RUN_FROM_PACKAGE. This means you will need to deploy your zip archive to public location and reference it by specifying a URL to the deployment package.

This can be done relatively securely using Azure Blob Storage and generating a SAS token that can be used in the URL to allow Azure Functions to download the package. Not great, but it works.

Alt

The full list of deployment options should be consulted as well if you have other plans in mind.

Alt

Azure CLI-based Deployment

After I left Terraform provision the environment, now I simply need to execute an Azure CLI command to deploy my code.

# create the deployment output
dotnet publish **/LocalFunctionProj.csproj \
    -r linux-x64 \
    -c Release \
    -o ./publish

# zip the output
cd publish
zip -r ../deployment.zip *
cd ..

# unzip to test - commented out because its not needed
# unzip deployment.zip -d staging

# deploy
az functionapp deployment source config-zip \
    -g rg-tsg-svc-assessment-dev \
    -n func-tsg-svc-assessment-dev \
    --src ./deployment.zip

The script basically uses dotnet publish to build the output files that will host our Azure Function. This is a ton of files and can be a bit intimidating. I have the dotnet publish command output these files into a directory called publish then I step into the publish directory and zip up its contents into a zip archive called deployment.zip . This ensures that when the zip archive is extracted the files are not contained in any folder. What’s in the root of the publish directory needs to be in the root of my zip archive. Finally, I use the Azure CLI’s az functionapp deployment source config-zip command to do zip archived based deployment.

My deployment reports that it was successful but when I go to the Azure Portal I find an empty page.

Alt

No amount of feverish smashing of the “Refresh” button or even the “Restart” button will result in the functions showing up in this list.

So what’s the problem?

False Positives and “Nothing to See Here”

The frustrating part of this issue was not only did I get almost no information about what was wrong. Everything seemed to be telling me all was well in the world — except my Azure Functions not showing up.

Alt

The deployment log says that my deployment was successful. When I logged into Kudu (SCM) I could see deployment zip archives in the right spot and even a text file that is used to specify which zip archive should be used. It all seemed to be OK.

Other People’s Pain

In my despair and foley I searched the interwebs high and low and found quite a few customers experiencing this issue. This was even not the first time this issue raised its ugly head at me. In the past I had overcome it by packaging the Linux app using the -r runtime flag. I even used the same zip command to zip up my publish directory.

Over the years, many have been biten by this issue, probably for one reason or another. I found it on Microsoft official message boards.

I found it on Stack Overflow

Maybe we should submit a feature request for better support in this issue.

Check your zipper

Well after much diagnosis, I found out it all came down to the files that were in my zip archive — or in my case, the ones that weren’t.

Turns out there is a hidden directory called .azurefunctions . This seems new to me. Maybe this is part of the new Azure Functions runtime.

Alt

If I modify my script to include all files — including hidden files — by simply adding literally two characters.

# create the deployment output
dotnet publish **/LocalFunctionProj.csproj \
    -r linux-x64 \
    -c Release \
    -o ./publish

# zip the output
cd publish
zip -r ../deployment.zip . *
cd ..

# unzip to test - commented out because its not needed
# unzip deployment.zip -d staging

# deploy
az functionapp deployment source config-zip \
    -g rg-tsg-svc-assessment-dev \
    -n func-tsg-svc-assessment-dev \
    --src ./deployment.zip

Pop Quiz! Which two characters did I add?

If you guessed . right before the * you’d be right! Yes, that is a . and a SPACE. This causes the hidden folder and all its sub-contents to be included in the zip archive.

Alt

Once I upload this to my Azure Function I see a totally different view!

Alt

Conclusion

Azure Functions on Linux can seem straightforward at a glance, but small details — like hidden directories in the public output — can cause them to vanish in the Azure Portal.

While overcoming this problem taught me the importance of carefully reviewing deployment artifacts, it also highlighted gaps in Microsoft’s diagnostic feedback. Clearer warnings and more transparent validation would help developers avoid falling into this trap, especially when dealing with multi-language microservices.

If you find yourself wrestling with similar issues, do not give up. Check your ZIP files carefully, confirm that your hidden directories are included, and consider submitting feedback to Microsoft and the Azure community. By calling for improved error handling and more instructive messaging, we can spark changes that benefit everyone who depends on a seamless Azure Functions deployment process.

Happy Azure Terraforming!