Setting up Dapr Redis NestJS Pub Sub Via Docker-Compose

Setting up Dapr Redis NestJS Pub Sub Via Docker-Compose

To start, I will just like to first say that **Dapr is amazing. **

It is a new system that allows better management of microservice application.

Managing interaction between microservices is really challenging, especially if you have a lot of services running.

Here is where Dapr really shines. Dapr handles all communication, rather than your services directly interacting/communicating with one another.

**All services will communicate and interact directly with Dapr. Dapr then does the rest of the work, forwarding what needs to be done to the appropriate service. **

It is also important to note that Dapr is language and platform agnostic. Meaning any programming platform just has to call the Dapr API via its native HTTP API using it's own native language and Dapr takes care of the rest.

This article focuses on running multiple Dapr services via docker-compose as much of the main documentation discusses running it via Kubernetes.

Mainly we would go through the Dapr publish and subscribe building block.

We will use Dapr to support pub/sub pattern between applications.

In this project, we will use Redis to publish to a NestJS subscriber through Dapr.

The difference in this project compared to traditional pub/sub is that Dapr completely manages the pub/sub.

So no need to worry about the implementation details between libraries.

**The Redis publisher publishes to Dapr and the Dapr determines which services are subscribed to the publish. Once it determines the services that are subscribed it calls the APIs of the server services. **

No need for both the publish and subcsription services to know each other's details(e.g. ports) and execute the exact language syntax required. Dapr manages it all for us.

STEP 1: Creating our project

First, we will create our project root folder to host all services we are going to create in the succeeding steps.

mkdir dapr-redis-nest-docker-pub-sub

STEP 2 : Creating our Dapr Placement service

Since we will be creating multiple services we will be using docker-compose to run these services.

Let's create our docker-compose.yml file in our project's root folder

cd dapr-redis-nest-docker-pub-sub
touch docker-compose.yml
version: "3.5"

services:
  dapr-placement:
    image: "daprio/dapr"
    command: ["./placement", "-port", "50006"]

Dapr placement service will be responsible for managing all the communication between the Dapr actors (our services).

To over simplify, it is responsible for routing all communication to the respective actor that is suppose to receive the communication. It functions as a message broker.

STEP 3: Creating our Redis Publish Service

Let's continue on modifying our docker-compose.yml file by adding our Redis service.

Add the following code to the services section of docker-compose.yml:

  redis-pub: 
    image: redis
    restart: always
   depends_on:
      -dapr-placement

STEP 4: Creating Dapr Pub-Sub Component

Create a dapr/components folder. Then create the component file redis-pubsub.yaml

mkdir -p dapr/components
cd dapr/components
touch redis-pubsub.yaml

Then open the file and insert the details of our Dapr pub/sub component

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: redis-pubsub
  namespace: default
spec:
  type: pubsub.redis
  version: v1
  metadata:
    - name: redisHost
      value: redis-pub:6379
    - name: redisPassword
      value: ""

The redisHost is the name of our Redis service redis-pub with default Redis port of 6379.

STEP 5: Creating our Redis Dapr Sidecar

As mentioned repeatedly in the previous sections, the services directly communicate with Dapr rather than directly with other services. Dapr serves as the middleman for all the services.

The services directly communicate with Dapr via their own Dapr sidecar which passes the communication to the Dapr placement which again passes it to the Dapr sidecar of the service that is suppose to receive the communication.

Add redis-dapr-sidecar service to our docker-compose.yml

 redis-dapr-sidecar:
    image: "daprio/daprd:edge"
    command:
      [
        "./daprd",
        "-app-id",
        "redis-pub",
        "-app-port",
        "6379",
       "-dapr-http-port",
        "5000",
        "-components-path",
        "/components",
        "-placement-host-address",
        "dapr-placement:50006"
      ]
    volumes: 
      - "./dapr/components/:/components"
    depends_on:
      - redis-pub
    network_mode: "service:redis-pub"

Here we assign the Dapr sidecar to redis-pub by using app-id also we use the redis port of 6379.

We also mount to the docker container the dapr/components table where redis-pubsub.yaml is found.

Dont' forget to declare dapr-http-port. This is the api of our Dapr sidecar allowing us to call various HTTP methods.

Defining your dapr-http-port is important as this is where you will be calling your various HTTP calls/methods/request (See Step 11).

Lastly, it is important that we attach the redis-dapr-sidecar to the redis-pub network namespace.

STEP 6: Creating our NestJS Server

We will use NestJS as our node server as our Redis subscriber.

Navigate to our project folder

cd dapr-redis-nest-docker-pub-sub

Then To set up a NestJS node server run the following commands:

npm i -g @nestjs/cli
nest new nest-sub

For this project we will chooseyarn as the package manager.

Next, we will set-up a post API endpoint. Dapr will call this endpoint once it receives a publish for our Redis service.

Go to nest-sub/src/app.controller.ts

Replace the code within this file with the following:

import { Controller, Post, Body } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post('/redis-pub')
  async postRedisPub(@Body() reqBody) {
    console.log(`Redis published ${JSON.stringify(reqBody)} `);

    return `${reqBody} published received by NestJS subscriber`;
  }
}

STEP 7: Creating a Dockerfile for our NestJS Subscriber Server

We will be running our NestJS server as a Docker container. We need to create a Dockerfile.

cd  nest-sub
touch Dockerfile

Then open the file and paste the following code:

FROM node:16.13.0-alpine

WORKDIR "/app"
COPY ./nest-sub/package.json ./
RUN yarn install
COPY ./nest-sub .
RUN yarn run build

EXPOSE 3000
CMD ["yarn","start:prod"]

STEP 8: Adding our NestJS Subscriber Service to docker-compose file

After creating our NestJS server and Dockerfile, we create our nest-subdocker service.

Add the following to the docker-compose.yml:

version: "3.5"

services:
  nest-sub:
    build:
      context: ./
      dockerfile: ./nest-sub/Dockerfile
    depends_on:
      - redis-pub 
      - dapr-placement
    restart: always

STEP 9: Create Dapr Subscription

We will define configuration for our pub/sub subscription.

Create a dapr/subscriptions folder. Then create the component file redis-subscription.yaml

mkdir -p dapr/subscriptions
cd dapr/subscriptions
touch redis-subscription.yaml

Then open the file and insert the details of our Dapr subscription component

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: nest-redis-sub
spec:
  topic: nest-redis-pub-topic
  route: /redis-pub
  pubsubname: redis-pubsub
scopes:
  - redis-pub

The route is the API that Dapr will call if a topic is published (See Step 6) .

The scope are the services which subscribes to the topic.

pubsubname is redis-pubsub which equals the metadata name defined in our redis-pubsub.yaml file (See Step 4).

In this project if a topic nest-redis-pub-topic published Dapr will call API /redis-pub in our nest-sub service.

STEP 10: Create our NestJS Server Dapr Sidecar

We need to create a side car for our NestJS Service just like the redis-pub service.

Add nest-sub-dapr-sidecar service to our docker-compose.yml

  nest-sub-dapr-sidecar:
    image: "daprio/daprd:edge"
    command: [
        "./daprd",
        "-app-id",
        "nest-sub",
        "-app-port",
        "3000",      
        "-components-path",
        "/components",
        "-placement-host-address",
        "dapr-placement:50006", 
      ]
    volumes:
      - "./dapr/components/:/components" 
    depends_on:
      - nest-sub
    network_mode: "service:nest-sub"

STEP 11: Testing if it works

Usually the Dapr Docker container will communicate within the Docker network.

But for us to do testing we will open map expose port 5000 to our local machine, consistent with http-port defined in redis-dapr-sidecar in Step 5

To do this just add the port mapping to the redis-pub service in our Dockerfile:

  redis-pub: 
    image: redis
    depends_on:
      - dapr-placement
    restart: always
    ports:
      - 5000:5000

Then in your terminal execute the following command:

 curl --location --request POST 'http://localhost:5000/v1.0/publish/redis-pubsub/nest-redis-pub-topic' \
--header 'Content-Type: application/json' \
--data-raw '{
    "test": "test"
}'

One of the positive things about Dapr is that it follows a specific URL format. Here we just use the Dapr sidecar HTTP port (5000), then the version number (v1.0), then the action(publish). Then what follows are the pubsubname (redis-pubsub) and topic (nest-redis-pub-topic) defined in our redis-pubsub.yaml configuration file.

Once the HTTP post request is made. Our NestJS Server should receive a post request at /redis-pub which will result to the following log:

dapr-cloudeventsjson-wrong.png

We can see that it is receiving the Redis publish via Dapr. But our NestJS server is not able to process the message properly.

Only a {} being published and not our published message.

We will address this issue in the next step.

NOTE: We are invoking the publish service via the dapr-http-port of our redis-dapr-sidecar. Usually there will be a separate Docker service (e.g. another server) which has its own Dapr sidecar which will call for the redis publish service. In such a case, we will use the Dapr sidecar http-port of that Docker service. The request will be sent by the sidecar to the Dapr placement service which will then determine the correct Dapr sidecar it will forward the request to.

STEP 12: Allow NestJS to parse application/cloudevents+json

The Content-Type of post request made by our nest-sub-dapr-sidecar to our nest-sub server will be application/cloudevents+json instead of application/json

Currently our NestJS server cannot parse application/cloudevents+json .

To solve this we need to first install body-parser:

cd nest-sub
yarn add body-parser

Next we need to modify our NestJS server main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as bodyParser from 'body-parser';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // this is added here to process Dapr.IO publish with content-header: appliction/cloudevents+json. If not included body of post request will be {}
  app.use(bodyParser.json({ type: 'application/cloudevents+json' }));
  // add this as other post with content-type: json will fail like login will fail due to bodyPaser code above
  app.use(bodyParser.json());
  await app.listen(3000);
}

bootstrap();

When we send the post request once again, our NestJS server will be able to process the request body and show the following log:

dapr-redis-publish-log-correct.png

There we go, we now have a working Redis Pub/Sub via Dapr.

You can clone the full repository via my Github repositiory:

https://github.com/niccololampa/dapr-redis-nest-docker-pub-sub

Until next time. Keep learning.

Stay stoked and code. :)


I hope you can voluntarily Buy Me A Coffee if you found this article useful and give additional support for me to continue sharing more content for the community. :)

Thank you very much. :)