Building Cloud-Native Services with Dapr, Go, and Kubernetes — Part 2

Vladimir Vivien
7 min readJun 1, 2024

Building stateful applications with Dapr

This series explores how to get started with Dapr to build distributed Go services deployed on Kubernetes.

The source code for the series is hosted at
github.com/vladimirvivien/dapr-examples

Building stateful services with Dapr

In this second part of this series on building applications with Dapr + Go + Kubernetes, we will demonstrate how to build a simple stateful service that uses the State Management building block of Dapr to store application data in a key/value data store.

The example showcases an application called Frontend Service which is a Go service that exposes two endpoints:

  • /orders/new - to create a new order
  • /orders/get/{id} — to retrieve information about a previous order

In this version of the service, the code uses the Dapr API to directly connect and store order data in a pre-configured key/value Redis data store that is deployed on the Kubernetes cluster.

Frontend service with Dapr-managed state management

The Dapr component APIs provides the necessary abstractions that make it easy to interact with any supported data store. This means you can swap out Redis for your preferred data platform for state management (you can learn more about Dapr Components here).

The full source code and configuration files for this post can be found on GitHub at github.com/vladimirvivien/dapr-examples/01-state-management

Pre-requisites

For this example you will need a local environment with Go and access to a Kubernetes cluster with the Dapr control plane components deployed (see Part 1 for detail). Next, let’s install the additional components needed for the example.

ko build tool

In this blog post series, we will use ko for compiling and building our Go source code into OCI-compliant images to be deployed on the Kubernetes cluster.

Helm deployment

You will need Helm to install certain components used in this example.

  • Follow instructions to install Helm locally

Redis on Kubernetes

Next, let’s use Helm to install Redis locally as shown in the followings steps:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install redis bitnami/redis

Make note of the DNS name of the Redis service as you will need it later for configuration.

The source code

Now that everything is installed, let’s highlight the source code for our frontend service to see how it works. First, let’s define some variables and types needed to manage incoming orders:

var (
appPort = os.Getenv("APP_PORT") // application port
stateStore = "orders-store" // Dapr ID for the configured data store
daprClient dapr.Client // Dapr API client instance
)

// Order type to store incoming order
type Order struct {
ID string
Items []string
Completed bool
}

The next code snippet declares our HTTP endpoints and starts the server:

func main() {
if appPort == "" {
appPort = "8080"
}

dc, err := dapr.NewClient()
...
daprClient = dc
defer daprClient.Close()

mux := http.NewServeMux()
mux.HandleFunc("POST /orders/new", postOrder)
mux.HandleFunc("GET /orders/order/{id}", getOrder)

if err := http.ListenAndServe(":"+appPort, mux); err != nil {
log.Fatalf("frontend: %s", err)
}
}

The next snippet implements a function handler for endpoint /order/new to handle HTTP posts for new orders. Notice how the code uses the Dapr client method daprClient.SaveState(…) to save the order into our configured state store (Redis in this instance):

func postOrder(w http.ResponseWriter, r *http.Request) {
var receivedOrder Order
if err := json.NewDecoder(r.Body).Decode(&receivedOrder); err != nil {
http.Error(w, "unable to post order", http.StatusInternalServerError)
return
}

orderID := fmt.Sprintf("order-%x", rand.Int31())
receivedOrder.ID = orderID
receivedOrder.Completed = true

// marshal order for downstream processing
orderData, err := json.Marshal(receivedOrder)
if err != nil {
http.Error(w, "unable to post order", http.StatusInternalServerError)
return
}

// Use Dapr state management API to save application state
// Use the orderId as key to save value as JSON-encoded binary
if err := daprClient.SaveState(r.Context(), stateStore, orderID, orderData, nil); err != nil {
http.Error(w, "unable to post order", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"order":"%s", "status":"received"}`, orderID)
}

The next snippet implements the HTTP handler for endpoint /orders/get/{id} to retrieve information for a previously saved order. The code uses the Dapr client API to retrieve the data using the daprClient.GetState(…) method call:

func getOrder(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")

// Use Dapr state management API to retrieve order by key
data, err := daprClient.GetState(r.Context(), stateStore, id, nil)
if err != nil {
log.Printf("get order data: %s", err)
http.Error(w, "unable to get order", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, string(data.Value))
}

Building a container image with ko

Now, let’s build and package the service’s source code as an OCI-compliant image using ko and deploy it to a local image repository (if you are publishing to remote repository, configure ko accordingly):

ko build --local -B ./frontendsvc

Next, check to see if the images are in your local image repository:

docker images
REPOSITORY             TAG              IMAGE ID       CREATED         SIZE
ko.local/frontendsvc latest 6094bfc88ad3 2 days ago 16.9MB

Since we’re using Kind for our example, let’s add the built image into our local cluster:

kind load docker-image ko.local/frontendsvc:latest --name dapr-cluster

If you are not using Kind, you will need to ensure that your cluster has access to your container image repository to pull your code.

Deploying the application

The application will use two manifest files:

  • One to deploy the Redis data store component
  • And one to deploy the application.

Both files are located in the manifest directory in the Github repo.

The Dapr Redis component manifest

The Dapr component manifest configures connectivity to the Redis on the cluster. The metadata.name attribute identifies the component and the spec.Type uses a Dapr-assigned value to identify the type of the component (state.redis).

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: orders-store
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: redis-master.default.svc.cluster.local:6379
- name: redisPassword
secretKeyRef:
name: redis
key: redis-password
auth:
secretStore: kubernetes

The remainder of the manifest file provides connection configuration to the Redis server. The redisHost is the DNS name of the Redis service which you got earlier. Note that you can use reference to Secrets to connect to resources running in Kubernetes.

Application deployment manifest

The frontend service’s OCI image will be deployed using a Kubernetes deployment manifest snippet as shown below.

apiVersion: apps/v1
kind: Deployment
metadata:
name: frontendsvc
spec:
...
template:
metadata:
labels:
app: frontendsvc
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "frontendsvc"
spec:
containers:
- name: frontendsvc
image: ko.local/frontendsvc:latest
env:
- name: APP_PORT
value: "8080"

Note that the deployment spec.template.annotations must include Dapr-specific annotations : dapr.io/enabled: true and dapr.io/app-id to signal to the Dapr controller to inject the Dapr sidecar container as part of the deployment.

Deploying the manifest files

Assuming both of these files are stored in a the manifest directory, they can be deployed together withe the following command:

kubectl apply -f ./manifest

Once the deployment is complete, you can verify that the Dapr components are running on the cluster with the following command:

dapr components -k

NAMESPACE NAME TYPE VERSION SCOPES CREATED AGE
default orders-store state.redis v1 2024-04-04 01:01.02 19d

Next, let’s use the kubectl CLI to verify the deployment of the application:

kubectl get deployments -l app=frontendsvc -o wide

NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
frontendsvc 1/1 1 1 75m frontendsvc ko.local/frontendsvc:latest app=frontendsvc

Next, check that there are 2 containers running in the application pod (one for our application and the other for the Dapr sidecar):

kubectl get pods -l app=frontendsvc

NAME READY STATUS RESTARTS AGE
frontendsvc-7c6bb8bf87-kpgvk 2/2 Running 0 3m3s

If you don’t see 2 that are in ready state, do some investigations to ensure that your application is running and find out why the sidecar may not be running.

Running the application

At this point, our application and its associated Dapr components are ready to start receiving HTTP requests. To keep things simple, we will do a port-forward to expose our container ports:

kubectl port-forward deployment/frontendsvc 8080

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

Next, we will use curl to post an order to the frontendsvc endpoint:

curl -i -d '{ "items": ["automobile"]}'  -H "Content-type: application/json" "http://localhost:8080/orders/new"

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 04 Apr 2024 00:54:21 GMT
Content-Length: 47

The the service responds with a JSON payload showing the status of the order:

{"order":"order-4d3d076e", "status":"received"}

Next, we’ll use the endpoint http://localhost:8080/orders/order/{id} to retrieve information about the order from the Redis data store:

curl -i  -H "Content-type: application/json" "http://localhost:8080/orders/order/order-4d3d076e"

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 04 Apr 2024 00:55:45 GMT
Content-Length: 63

The result is JSON-encoded payload that shows information about order:

{"ID":"order-4d3d076e","Items":["automobile"],"Completed":true}

Next step

In this post we walked through the steps necessary to build a simple Go service that uses the Dapr State Management API to store and retrieve application data using a Redis instance on Kubernetes.

In the next post we will continue to refine our example and show how use Dapr to invoke another service running in Kubernetes.

References

--

--