Dapr and Azure Functions: Part 3 – Containerizing with Dapr

In Part 2, we created the Dockerfile and deployed the Functions app to a container.

Now we want to put it all together with Dapr.  We’re going to start with a bit of housekeeping and reorganizing our project.

Step 1: Refactor the Solution

Our first step is to refactor the solution.

If we simply add the Dapr container, our solution will look like this:

It is important to note that in this case, the Functions Job Host (FJH) is exposed directly via port 8181 on the host which is mapped to port 80 on the container where the FJH is listening; we do not have access to the daprd container directly unless we do some networking ninjutsu to expose it via a port on the host.

For this example to make any sense, we need to create another service which the FJH will interact with so our goal is something like this:

We’re going to add another “backend” service which simply provides the date to the existing “frontendHelloWorld service.

In the current project, create a new folder structure like this:

  • services
    • hello (move all current files here)
    • date (a new folder for the second service)

From the terminal, switch into the date directory and we’ll create another Function app.

func init --name DateFunc --worker-runtime dotnet

Add another Function:

func function new --name CurrentDate --authlevel anonymous

We’ll update this Function to just return the current date:

We’ll also go ahead and add the Dockerfile (could have been done at func init, too):

func init --docker-only

Step 2: Invoke the Service via Dapr

What we want to do now is to invoke this service not directly through its HTTP endpoint, but through Dapr service invocation.

From HelloWorld.cs, we’re going to get the current date and add it to our output message.

First, we switch into the /services/hello directory in the terminal and execute:

dotnet add package dapr.client

NOTE: After adding the package in VS Code, intellisense is broken until you reload the project or CTRL+SHIFT+P and run Developer: Reload Window

Now we are going to get the current date from the service:

You can find more information about the service invocation in this documentation, but the gist of it is that the HttpClient that has been constructed will translate the outbound URL from:

http://dateapp/api/CurrentDate

to:

http://127.0.0.1:3500/v1/invoke/dateapp/method/api/CurrentDate

Pay special attention to the dateapp string.

Note that in production, it is recommended to keep the HttpClient long-lived and inject it into the Function (for an example, check my AzFunctionsGraphQL GitHub repo or the Microsoft documentation on Functions Dependency Injection).

At this point, we can test this by running the two apps separately using dapr without deploying everything to Docker:

  1. From /services/date run dapr run --app-id dateapp --app-port 7081 -- func start -p 7081
  2. From /services/hello run dapr run --app-id helloapp --app-port 7071 --dapr-http-port 8181 -- func start -p 7071

Take special note of the -- in the commands before the func since we need this to tell it to use a different port; if you don’t include this, it will ignore the -p parameter value and you will have a port conflict since dapr run starts the functions runtime in the host context (so both instances of func.exe will try to bind to a single port).

In the browser, we can access it like this: http://localhost:7071/api/HelloWorld

We get the default message with the current date time 🎉

This is perhaps the best way to work during development as it eases debugging and you can cycle individual services.

Now that we’ve confirmed that everything is working, let’s pull this all together in a Docker Compose deployment.

Step 3: Add Docker Compose File

The Docker Compose File (DCF) is used to bring up/down the full set of artifacts in the Docker runtime.

Once we have this set up, we can execute docker-compose up to bring up the full set of containers.

Start by creating a file at the root called docker-compose.yml.

(NOTE: It is extremely helpful to have the Docker Compose reference handy; this documentation is really, really lacking for the volume of knowledge that you need to understand the complexity, but it’s better than nothing!)

Your file tree should look like this:

I think that this was really the first wall of complexity for me so we’re going to spend some time here breaking down what we need to do in this file.

The structure of the file at a high level will look like this:

It is important that you start from this baseline as it will be foundational for understanding each piece that gets added.

Step 3.1: Set Up helloapp

First we set up helloapp.  This is the Functions app that will respond to the browser invocation:

The image name will look like this once published at the end:

Let’s break down each line:

  • context is the working directory to use when executing the build command.  If you use only ., the build will fail so use the full path OR modify the Dockerfile
  • dockerfile is the name of the Dockerfile
  • 8181:80 8181 is an arbitrary port on the host machine to bind to port 80 on the container which is where the Functions Host Runtime is listening by default.  So for us to access this, we need a mapping.
  • hello-dapr identifies the network that this app belongs to. This is optional; if we do not specify a network at all, then a default one will be assigned but that can make it difficult to troubleshoot issues with networking if needed!

Step 3.2: Set Up helloapp-dapr

Now we connect the side car to the app:

  • command is the command that will be executed in the container once it starts up
    • helloapp is the name that is assigned to the Dapr service endpoint and it must match the URLs used in your service invocations.
    • 80 identifies the port that the app (Functions Host Runtime) will be listening on (we’ll look at this in a moment)
  • helloapp identifies the dependency
  • service:helloapp connects the network to the same network as the app

When everything is hooked up correctly, you should see the following message in the console output for the sidecar when we start it up:

msg="application protocol: http. waiting on port 80. This will block until the app is listening on that port."

Then when it detects the Functions Host Runtime on port 80, you’ll see:

msg="application discovered on port 80"

If you are blocked at the first message, then you will need to review the configuration to get all of the parts connected.

Step 3.3 Set Up dateapp

Now we set up the container for the date service app that will provide the current date/time:

We don’t need to cover each line here, but let’s take a look at the key differences:

  • expose instead of ports.  The dateapp is a “backend” service and isn’t exposed to the host.  In this case, we only need to expose it to the internal network of the cluster.  You can read more about the expose option in the Docker Compose documentation.
  • 80 like before is the port that the Functions Runtime Host is listening on.

Step 3.4 Set Up dateapp-dapr

And the final piece is almost identical to the previous helloapp-dapr:

Step 4: Running it All

Now we’re ready to run this bad boy.

We can do this using CTRL+SHIFT+P and Docker: Compose Up or from the terminal at the root directory, use docker-compose up

This should start building the images and bring everything online:

We can see that there is one port exposed at 8181for dapr-func_helloapp_1.  If we load the browser and hit the URL: http://localhost:8181/api/HelloWorld, we should see the following:

If we take a look at the diagram we started with, it all comes together now:

From our entry point on localhost:8181, we can interact with an HttpTrigger Function that will invoke the dateapp service via a Dapr Sidecar!  We haven’t added any components yet; at this stage, I hope you have a solid understanding of the fundamentals of how to put the pieces together!

Next Part

Next, we want to do the same with Kubernetes!

Addendum: Notes

NOTE: When modifying code, use docker compose build to repackage the images.

You may also like...

3 Responses

  1. July 2, 2021

    […] Now to add Dapr to our container! […]

  2. July 2, 2021

    […] Part 3 – Containerizing with Dapr […]

  3. July 6, 2021

    […] In Part 3, we deployed the services to Docker using Docker Compose. […]