DEV Community

Cover image for Durable Functions with Netherite on Kyma
Christian Lechner
Christian Lechner

Posted on

Durable Functions with Netherite on Kyma

Intro and Goal

In this blog post I would like to describe my journey with Netherite as storage provider for Azure Durable Functions and the deployment of this combination to a Kubernetes cluster. My main goal was to get things running and see where there are some rough edges. Consequently, this blog post does not serve as a guide for a productive setup, it is more a first step towards such a setup.

Remark: If you have not yet heard about Netherite as a storage provider for Azure Durable Functions I recommend to start here 🧐: https://microsoft.github.io/durabletask-netherite/#/

Sample Code

All the code used for this blog post is available on GitHub under:
https://github.com/lechnerc77/netherite-kyma-sample

Setup

The setup for the journey is quite basic. We use the Durable Functions sample that comes along with the Azure Functions Extension of VS Code. We will use TypeScript as language. You should be able to do the same with C#/.NET Core 3.1, but I use a different language than that, as there are often some surprises when leaving the .NET area.

Code-wise we have an HTTP Starter Function that triggers the Orchestrator Function. This Function then calls an Activity Function three times with different parameter values:

import * as df from "durable-functions"

const orchestrator = df.orchestrator(function* (context) {
    const outputs = []

    outputs.push(yield context.df.callActivity("HelloCity", "Tokyo"))
    outputs.push(yield context.df.callActivity("HelloCity", "Seattle"))
    outputs.push(yield context.df.callActivity("HelloCity", "London"))

    return outputs
})

export default orchestrator
Enter fullscreen mode Exit fullscreen mode

The Activity Function returns a string containing the parameter handed over to the Function:

import { AzureFunction, Context } from "@azure/functions"

const activityFunction: AzureFunction = async function (context: Context): Promise<string> {
    return `Hello ${context.bindings.name}!`
}

export default activityFunction
Enter fullscreen mode Exit fullscreen mode

This setup runs on the "usual" Azure Storage. Now let us bring that over to Netherite.

Transfer to Netherite

As Netherite is not yet supported via the extension bundle mechanism, we remove the extension bundle section from the host.json and add the extension for Netherite via:

func extensions install  --package Microsoft.Azure.DurableTask.Netherite --version 0.5.0-alpha`
Enter fullscreen mode Exit fullscreen mode

This gives us a extensions.csproj file containing the relevant dependencies.

In addition, we add a configuration in the host.json file to make the host aware of the new storage provider:

"extensions": {
    "durableTask": {
      "hubName": "HelloNetherite",
      "useGracefulShutdown": true,
      "storageProvider": {
        "type": "Netherite",
        "StorageConnectionName": "AzureWebJobsStorage",
        "EventHubsConnectionName": "EventHubsConnection",
        "CacheOrchestrationCursors": "false"
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

⚠ Be aware of the parameter "CacheOrchestrationCursors": "false": this setting is necessary for the non-.NET world to keep the orchestration running. Otherwise your processing will abort after the first yield (see https://github.com/microsoft/durabletask-netherite/issues/69)

This is a very lean configuration that makes things work, but there are a lot more finetuning possible as laid out in the documentation of Netherite.

Try out locally

As a first test we run the function locally, so we must adjust the local.settings.json to point to the local storage emulator. In addition (and in contrast to “classical” Durable Functions) we also need to specify the connection to the EventHub which for local execution points to the memory:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "EventHubsConnection": "MemoryF",
    "FUNCTIONS_WORKER_RUNTIME": "node"
  }
}
Enter fullscreen mode Exit fullscreen mode

As storage emulator we use Azurite i. e. the Azurite VSCode extension.

Executing the Function should produce the expected output:

Netherite Local Function execution

Build the Docker Image and run it

As the local setup works, it's time to build the Docker image with the Azure Function inside. The func CLI helps us with that via:

func init --docker-only
Enter fullscreen mode Exit fullscreen mode

This command creates the Dockerfile as well as the .dockerignorefile

When using the Azurite VSCode extension, several files have been created by it. To avoid copying them to the Docker image, we put the following lines into your .dockerignore file:

__azurite_db*__.json
__blobstorage__
__queuestorage__
Enter fullscreen mode Exit fullscreen mode

With that we can build your image. We use a Makefile for the build and push that looks like this:

RELEASE=0.0.1
APP=containered_netherite
DOCKER_ACCOUNT=<YOUR DOCKER ACCOUNT NAME>
CONTAINER_IMAGE=${DOCKER_ACCOUNT}/${APP}:${RELEASE}

.PHONY: build-image push-image

build-image:
    docker build -t $(CONTAINER_IMAGE) --no-cache --rm .

push-image: build-image
    docker push $(CONTAINER_IMAGE)
Enter fullscreen mode Exit fullscreen mode

To validate that the container is running as expected, we create the necessary resources on Azure using the scripts provided in the samples of the Netherite repository (https://github.com/microsoft/durabletask-netherite/tree/main/samples/scripts) namely the init.ps1 script to create the Azure storage as well as the Event Hub. Before running them make sure that you adjusted the settings.ps1 file as needed esp. put in a fitting name (or names) for the resources.

After successful creation of the resources we take the connection strings available (e.g. in the Azure portal) for the two resources and put them in a env.list file. This makes the injection into the container easier.

We put that commando to start the container also in the in the Makefile to have everything around Docker in one place:

docker run --env-file env.list -it -p 8080:80 $(CONTAINER_IMAGE) 
Enter fullscreen mode Exit fullscreen mode

When we spin up the container we will see … an error:

Microsoft.Azure.WebJobs.Host.FunctionInvocationException: Exception while executing function: 
 ---> System.InvalidOperationException: Webhooks are not   
   at Microsoft.Azure.WebJobs.Extensions.DurableTask.HttpApiHandler.ThrowIfWebhooksNotConfigured()
Enter fullscreen mode Exit fullscreen mode

Hmm … doing some serious senior deveveloper research to figure that out 🤪 aka google-fu and searching on stack overflow , I came across this: https://stackoverflow.com/questions/64400695/azure-durable-function-httpstart-failure-webhooks-are-not-configured/64404153#64404153

So we need one more parameter namely the WEBSITE_HOSTNAME to inject into the container to get things going. The parameter must point to the HTTP hostname used by the Azure Function.

For the local execution via Docker this means to add the following line to the env.list:

WEBSITE_HOSTNAME=localhost:8080
Enter fullscreen mode Exit fullscreen mode

With this adjustment the Docker container works as expected. We now push it to the Docker registry via the corresponding command in the Makefile.

After that let us move to the logical next step and bring the thing to Kubernetes.

Deploy it to Kubernetes … ehh Kyma

I am using Kyma as opinionated stack on top of Kubernetes (to be precise on a Gardener cluster) for this exercise. The setup should be similar for vanilla stacks, but the API gateway must be adjusted accordingly depending on what you use.

Remark: In case you want to try out Kyma you can do so for free via the SAP Business Technology Platform trial.

For the deployment to Kyma we need the following files:

  • deployment.yaml: containing your app aka container and the references to the config map and the secrets and the service
  • secrets.yaml: containing the connection strings to the Azure storage and the Event Hub
  • apirule.yaml: containing the configuration of the API Gateway provided by Kyma to expose the HTTP endpoint. Attention - the API rule has no authentication in it!

After applying those files to the Kyma cluster via kubectl apply -f we need to look up the endpoint at which the API is hosted in the Kyma Dashboard and add that to the configmap.yaml to provide the endpoint of the WEBSITE_HOSTNAME parameter. After applying this last file, the setup should be up and running. So giving it a try we see output like this when the Starter Function is executed:

Azure Function Starter Result

Navigating to the status URI we will see the expected result:

Durable Function Execution Result

Mission accomplished 🥳: Durable Functions with Netherite as storage provider are up and running on Kyma (Kubernetes)!

Cleanup

As the resources on Azure will cost money you can clean things up using the script delete.ps1 from the GitHub repository which will delete the complete resource group on Azure.

Be aware that when you restart the services to exchange the connection strings in the secrets.yaml file and re-apply it to your Kubernetes deployment.

Summary

Although Netherite is still in alpha, you can already make your hands dirty with it. In this blog post we went through a scenario where this storage provider use-case is deployed to Kubernetes. Besides two small obstacles we had to overcome to get things running the setup works as expected.

However, this is just the very first step when using Netherite and there are some more steps to move forward to achieve a production grade setup.

One future topic that certainly makes sense is the integration of the Azure services in a more “natural” way into the Kubernetes cluster and not just via calls to the outside world. So, stuff for more blog posts - see you then 🤠

Discussion (0)