Project Fn provides a framework for creating and running serverless functions. It is the foundation for the Functions service on Oracle Cloud Infrastructure. Dapr.io is an open source project that provides a powerful personal assistant for any application and a distributed application runtime that especially shines with microservices. Some aspects of Dapr.io do not readily apply to Fn style serverless functions – such as subscription to message topics – but others can be quite useful. Dapr provides a large collection of bindings to dozens of technologies and cloud services that make it quite easy for any application to interact with this broad spectrum of components.
With Dapr, an application has a sidecar – a companion process (running Dapr) that acts as a know-it-all proxy. With simple HTTP or gRPC calls, the application requests Dapr to make the technology and service specific calls on its behalf and share the result.
The application does not get bloated with technology specific plumbing, its developers do not need to know APIs and interaction details for specific technologies and switching between techn0logies becomes much easier. Note: the diagram shows a Node application but it is important to realize that the interaction between the application and Dapr is via HTTP or gRPC and any application that can interact over HTTP can leverage Dapr.
The objective in this article is to use Dapr in Fn functions. These are serverless functions that are based on a container image. When the function is triggered, a container is started from the image and the application inside the container is executed and will handle the triggering event. When the response is produced, the container will continue to run (stay hot) for a little while in case there will be another request to handle. After several minutes of inactivity, the container will be stopped until the next function request arrives.
This article provides a simple application that demonstrates how Fn functions can be enhanced with the powers of Dapr.io. This includes:
- installing the Dapr runtime into the Function container
- configure some Dapr components
- run Daprd runtime as a “sidecar” (parallel process) alongside the Fn function
- invoke the Dapr Sidecar from Fn function to trigger the components in order to interact with external services
The application is a simple one – not meaningful regarding its functionality. It should make clear what is to be done for connecting an Fn function to Dapr. I have picked Node as the function implementation language. This choice is arbitrary – most of what is done here should work in a similar fashion for other FDKs, such as Java, Python, Go. The range of external services to interact with can also easily be expanded – have the function interact with Oracle Database, MySQL, SQL Server, AWS S3, Apache Kafka, Twitter, SMTP etc. All can easily be added – with a simple yaml file to configure the external service and a straightforward HTTP exchange from application to Dapr sidecar.
All sources discussed in this article are available in this GitHub Repo: https://github.com/lucasjellema/fn-dapr .
Building the Function Container Image
The Fn container images can be built using the Fn command line interface. This process usually starts from the predefined Fn container images for each of the supported languages using a predefined Dockerfile to build the container. It is also possible to build a container using a custom Dockerfile (and even using a custom container image as starting point – using the so called hot wrap approach).
In this case I use a custom Dockerfile. In order to have the Fn CLI recognize this Dockerfile, I have specified in the func.yaml file with meta data regarding the function that the runtime to be used for the function is docker.
The objective is to have the daprd executable installed into the Function container image and to have the executable started when the container starts – next to the application that handles function requests.
The build process consists of two stages, using a build container (development time only) in which resources are collected and prepared and the final container image intended for runtime. I have extended the Dockerfile to install the Dapr CLI and initialize Dapr with the slim install. The resulting daprd executable file is subsequently used when the runtime container image is assembled.
The runtime container image is extended with the installation of the daprd executable into the function’s home directory, setting of the environment variable DAPR_HTTP_PORT on which the Dapr Sidecar will listen for instructions from its companion application and finally the CMD to run the start:dapr script through npm (from the package.json file).
This script contains the following command – to run both Dapr Sidecar and the Node Function application:
“start:dapr”: “/function/daprd –app-id daprized-function –dapr-http-port $DAPR_HTTP_PORT –components-path ./components/ & npm run start”
This instruction tell Dapr to run, listen to application instructions on port $DAPRP_HTTP_PORT and read component definitions from all yaml files in directory /function/components. It also runs the start script in package.json through npm in a parallel process thread. This starts the Node application that implements the function. This application registers itself with the Fn FDK for handling incoming function triggers.
The Dockerfile in its entirety is shown here (see GitHub Repo)
To build the container image for the function that will have the Dapr executable inside and will start with Dapr running alongside the function application, these are the steps using the Fn CLI:
fn build –v
To deploy the function to the local Fn server (that was started with “fn start –log-level DEBUG” in order to have full visibility in order to learn what is happening inside the container):
fn deploy –create-app –app tutorial –local –no-bump
And now the function can be invoked – at its most simplest using:
fn invoke tutorial daprized-function
The response is constructed using the greetings fetched through Dapr from a file on GitHub and the fruit information is retrieved by Dapr on behalf of the Function from a GraphQL API. The Function does not know about either GitHub (and HTTPS communication) or about the GraphQL API and its whereabouts. In both cases, it just calls upon its Dapr personal assistant to do the job of invoking the external services.
Leveraging Dapr from the Function
In order for the Function to make use of Dapr it invokes Dapr on the port specified through $DAPR_HTTP_PORT. Dapr has a set of APIs for invoking bindings, interacting with state stores, working with Pub/Sub brokers etc. These APIs are called using simple HTTP calls.
The configuration of the external components for Dapr to interact with is defined in yaml files in the components subfolder under /function. In this example application, only two components are defined. One is of type bindings.http and simply sends HTTP or HTTPS requests to an external endpoint. The endpoint is configured in the yaml file – the application does not (need to) know where it is or if it may have changed at some point (which is great for mock testing among other things)
The application needs to know the name – greetings – of the component in order to have Dapr invoke it on its behalf.
The other component used in this example is a GraphQL API that returns Fruit information. it is configured in yaml:
Again, all the application needs to know to invoke it is its name – fruits.graphql – and of course what kind of query it can send to this API.
Dapr has an SDK for Node that provides the most convenient method for interacting with the Dapr sidecar in a Node application. I tried to use it, but ran into an error that I could not easily resolved: “ReferenceError: globalThis is not defined – (/function/node_modules/dapr-client/actors/runtime/ActorRuntimeConfig.js:3:20)” I therefore decided to fall back to straightforward HTTP requests to the sidecar.
This is the generic function for making any call to the Dapr sidecar:
With the daprizedCall function at our disposal, it becomes easy to retrieve a greeting from one external component and fruit data from another:
These calls have the name of the binding as input (as defined in the yaml files in the components subdirectory) as well as the data to provide to Dapr in order to specify the operation to perform and the payload to process.
For completeness sake is here the main function in this Node application:
This is where Function invocations first end up from the Fn runtime framework.
Conclusion
Using a slightly amended build process for Fn Functions, it turns out to be quite possible to add Dapr to Function Container Images and run Dapr sidecar as a companion process to a Function application. This allows the function to leverage Dapr as a proxy for making calls to components in many different technologies and offered on different platforms and cloud infrastructures. This makes development (and testing) much easier. Of course there is a price to be paid: running the sidecar requires resources and installing the daprd application in the function container takes up close to 100 MB in storage. This might be a price worth paying when the value of Dapr is fully leveraged.
Resources
GitHub Repository with the sources for this article: https://github.com/lucasjellema/fn-dapr
Dapr.io Home page – https://dapr.io/ –
How to take control over building of the Function container – Creating a Function from a Docker Image https://fnproject.io/tutorials/ContainerAsFunction/
How to install Dapr runtime into a custom container – Running Dapr on Azure IoT Edge https://xaviergeerinck.com/post/2021/4/23/iot-dapr-iot-edge
Troubleshooting and Logging with Fn – https://fnproject.io/tutorials/Troubleshooting/
Dapr HTTP binding component – https://docs.dapr.io/reference/components-reference/supported-bindings/http/
Dapr GraphQL binding component https://docs.dapr.io/reference/components-reference/supported-bindings/graghql/
Fruits GraphQL API https://fruits-api.netlify.app/graphql
Dapr arguments and annotations for daprd, CLI, and Kubernetes https://docs.dapr.io/reference/arguments-annotations-overview/
Install packages in Alpine docker – https://stackoverflow.com/questions/48281323/install-packages-in-alpine-docker
Dapr support for Oracle Cloud Infrastructure Object Storage Service – https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-oci-objectstorage/
Fn Hot Wrap – using a custom Docker Container Image – https://fnproject.io/tutorials/docker/CustomLinuxContainer/