Build microservices with Dapr in Kubernetes
Thy Pham
Posted on February 20, 2022
Introduction
Dapr is a Cloud Native Computing Foundation project currently at the Incubating stage. It was created to help us as developers build microservices quickly with ease.
Dapr runs as a sidecar and brings us some benefits, such as:
- Provides an API to make it easier to build the connections between services like REST/gRPC, Pub/Sub, etc.
- Includes best practices for building distributed applications.
- Make your app portable, which means it can connect to different databases and secret stores, send/receive messages to/from various Pub/Sub brokers, etc., without changing the code.
To see how Dapr works, let's build a simple Order service with a REST endpoint for creating new orders. The service stores the orders into a Redis database.
We will also build a client service that consumes this endpoint. All two services are written using Node.js and Express.
We will then run the two apps in Kubernetes with Dapr sidecar. There are many other use cases of Dapr. I suggest going to its homepage for more details.
Implementation
Before following the steps below, I assume that we all have a running Kubernetes cluster. I use minikube to run a Kubernetes cluster on my local machine for testing purposes.
Step 1: Run Redis in Kubernetes
To make it simple, we can use Helm to install Redis.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install redis bitnami/redis
Step 2: Deploy Dapr control plane to the cluster
First, follow this instruction to install Dapr CLI.
After the installation is done, let's deploy the Dapr control plane to our k8s cluster:
> dapr init --kubernetes --wait
We can now see Dapr has been installed in namespace dapr-system.
You can also install Dapr in another namespace by using flag -n <yournamespace>
.
Step 3: Create a Dapr state store component to let Dapr connect to Redis.
To make Dapr recognize and connect to Redis, we have to create a Dapr component by writing a YAML file:
# file Redis.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
# We will use this name to configure the dapr-client SDK
# in our Order service code so Dapr sidecar can connect
# to this Redis store
name: redis-store
spec:
type: state.redis
version: v1
metadata:
# The Redis host and password below are the default
# values after installing Redis using Helm. You should
# provide your values if you have your own Redis instance.
- name: redisHost
value: redis-master:6379
- name: redisPassword
secretKeyRef:
name: redis
key: redis-password
Apply the component:
> kubectl apply -f redis.yaml
Step 4: Build Order service.
Now it's time to write our Order service. In the code below, I use Dapr SDK to store data in Redis through Dapr sidecar running locally. Note that at this line: const STATE_STORE_NAME = "redis-store"
. The store name here must be the same as the component name defined in Redis.yaml file.
const express = require("express");
// Dapr SDK
// > npm install dapr-client
const { DaprClient, CommunicationProtocolEnum } = require("dapr-client");
const app = express();
const port = 3000;
// Our Order service only interacts with Dapr sidecar running in local.
const daprHost = "127.0.0.1";
// Create a new Dapr client
const client = new DaprClient(
daprHost,
// The HTTP port that the Dapr sidecar is listening on.
// We should use this env var to connect to Dapr sidecar
process.env.DAPR_HTTP_PORT,
CommunicationProtocolEnum.HTTP
);
// Name of the state store component, defined in Redis.yaml file.
const STATE_STORE_NAME = "redis-store";
app.use(express.json());
app.post("/orders", async (req, res) => {
const order = req.body;
// We can use this Dapr client to store data into database.
const response = await client.state.save(STATE_STORE_NAME, [
{
key: new Date().getTime().toString(), // Generate a unique key.
value: order,
},
]);
res.json(response);
});
app.listen(port, () => console.log(`server listens on port ${port}`));
As we can see, our Order service doesn't need to know anything about Redis, and it can still interact with it. This makes our Order service portable.
I have built a Docker image for the Order service, and it is available on Docker Hub at tphamdev/dapr-example-order-service. For more details about the steps to create a Docker image and push it to the Docker Hub, you can have a look at this post here.
Step 5: Create a K8s Deployment for our Order service
Let's write a Deployment file for our Order service. You can see I use three annotations, dapr.io/enabled,
apr.io/app-id
and dapr.io/app-port
in the file below. These annotations let the Dapr control plane knows that it should deploy a sidecar to our Order service Pod.
# file order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service-deployment
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
# We add the annotations below to let Dapr recognize
# and deploy the sidecar together with our service in the pod.
annotations:
dapr.io/enabled: "true"
# The client service will use this name to locate
# the Order service through the Dapr sidecar.
dapr.io/app-id: "order-service"
# The port that your application is listening on
dapr.io/app-port: "3000"
labels:
app: order-service
spec:
containers:
- name: order-service
image: tphamdev/dapr-example-order-service
Apply the deployment:
> kubectl apply -f order-service-deployment.yaml
Step 6: Build a client to consume the Order service endpoint
Let's create a simple client service that calls the /orders
endpoint from the Order service above to create new orders.
This client service will use Dapr SDK to interact with the Dapr sidecar running on localhost. The Dapr sidecar will take care of the rest (connect to the Order service, also through another Dapr sidecar).
// Dapr SDK
// > npm install dapr-client
const { DaprClient, HttpMethod } = require("dapr-client");
async function main() {
// Our client service only connects to the Dapr sidecar running locally.
const daprHost = "127.0.0.1";
// The HTTP port that the Dapr sidecar is listening on.
// We should use this env var to connect to Dapr sidecar
const daprPort = process.env.DAPR_HTTP_PORT;
// Create a new Dapr client
const client = new DaprClient(daprHost, daprPort);
// This is the value of the dapr.io/app-id annotation,
// which we defined in order-service-deployment.yaml file.
const serviceAppId = "order-service";
// The REST Endpoint /orders
const serviceMethod = "orders";
for (let i = 1; i < 100; i++) {
// New order
const order = {
name: `order-${i}`,
};
// Invoke the Order service using Dapr SDK (through Dapr sidecar)
const response = await client.invoker.invoke(
serviceAppId,
serviceMethod,
HttpMethod.POST,
order
);
console.log(`order-${i} was created:`, response);
await sleep(2000);
}
}
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
main();
The Docker image of the client is tphamdev/dapr-example-order-client
Step 7: Create K8s Deployment for the client service.
The deployment file is the same as the order-service-deployment.yaml
file in Step 5 above. We also add some annotations to let Dapr recognize and deploy the sidecar.
# file order-client-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-client-deployment
spec:
replicas: 3
selector:
matchLabels:
app: order-client
template:
metadata:
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "order-client"
labels:
app: order-client
spec:
containers:
- name: order-client
image: tphamdev/dapr-example-order-client
Apply the deployment:
> kubectl apply -f order-client-deployment.yml
Confirm that the pods are running:
> kubectl get pod
❯ kubectl get pod
NAME READY STATUS RESTARTS AGE
order-client-deployment-55948499c8-4snfg 2/2 Running 0 11s
order-client-deployment-55948499c8-b9g68 2/2 Running 0 11s
order-client-deployment-55948499c8-fz5rr 2/2 Running 0 11s
order-service-deployment-59f7b4c5cd-7g87k 2/2 Running 0 2m33s
order-service-deployment-59f7b4c5cd-ddlrs 2/2 Running 0 2m33s
order-service-deployment-59f7b4c5cd-hg52f 2/2 Running 0 2m33s
redis-master-0 1/1 Running 1 (112m ago) 19h
redis-replicas-0 1/1 Running 2 (111m ago) 19h
redis-replicas-1 1/1 Running 2 (111m ago) 19h
redis-replicas-2 1/1 Running 2 (111m ago) 19h
Let's see the logs of order-client:
> kubectl logs order-client-deployment-55948499c8-4snfg order-client -f
order-1 was created:
order-2 was created:
order-3 was created:
order-4 was created:
order-5 was created:
order-6 was created:
order-7 was created:
order-8 was created:
order-9 was created:
...
Now let's confirm what we have in Redis.
> kubectl exec -it redis-master-0 -- sh
$ redis-cli -a "password from Helm"
127.0.0.1:6379> keys *
1) "order-service||1645345085506"
2) "order-service||1645345149800"
3) "order-service||1645345188626"
4) "order-service||1645345150485"
5) "order-service||1645344965614"
127.0.0.1:6379> hgetall "order-service||1645345085506"
1) "data"
2) "{\"name\":\"order-8\"}"
3) "version"
4) "1"
127.0.0.1:6379>
Yay! The Order service has successfully stored data into Redis.
Posted on February 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.