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-sub
docker 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:
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:
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. :)