Build microservices with Dapr in Kubernetes
Tags: kubernetes, architecture, tutorial, devops
Published: 2022-02-20 08:57:32

## Introduction

[Dapr](https://dapr.io/) is a [Cloud Native Computing Foundation](https://www.cncf.io/) 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](https://redis.io/) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65co43whdzet1x3y8035.jpg)

## Implementation

Before following the steps below, I assume that we all have a running Kubernetes cluster. I use [minikube](https://minikube.sigs.k8s.io/docs/start/) 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](https://docs.dapr.io/getting-started/install-dapr-cli/) 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](https://docs.dapr.io/concepts/components-concept/) by writing a YAML file:

```yaml
# 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.

```javascript
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](https://hub.docker.com/repository/docker/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](https://dev.to/thyphamdev/how-to-run-node-app-in-kubernetes-5ehk).

### Step 5: Create a K8s Deployment for our Order service

Let's write a [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/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.

```yaml
# 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).

```javascript
// 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](https://hub.docker.com/repository/docker/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.

```yaml
# 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.