DEV Community

Thy Pham
Thy Pham

Posted on

Build microservices with Dapr in Kubernetes

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:

  1. Provides an API to make it easier to build the connections between services like REST/gRPC, Pub/Sub, etc.
  2. Includes best practices for building distributed applications.
  3. 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.

Our services architecture

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply the component:

> kubectl apply -f redis.yaml
Enter fullscreen mode Exit fullscreen mode

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}`));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply the deployment:

> kubectl apply -f order-service-deployment.yaml
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Apply the deployment:

> kubectl apply -f order-client-deployment.yml
Enter fullscreen mode Exit fullscreen mode

Confirm that the pods are running:

> kubectl get pod
Enter fullscreen mode Exit fullscreen mode
❯ 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
Enter fullscreen mode Exit fullscreen mode

Let's see the logs of order-client:

> kubectl logs order-client-deployment-55948499c8-4snfg order-client -f
Enter fullscreen mode Exit fullscreen mode
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:
...

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

Yay! The Order service has successfully stored data into Redis.

Discussion (0)