Building Cloud-Native Services with Dapr, Go, and Kubernetes — Part 2
Building stateful applications with Dapr
This series explores how to get started with Dapr to build distributed Go services deployed on Kubernetes.
- Part 1 — Getting started with Dapr on Kubernetes
- Part 2 — Building stateful applications with Dapr
- Part 3 — Creating and invoking Dapr-enabled services
- Part 4 — Creating event-based services with Dapr
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.
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.
- Install ko on your machine
- Learn how to build and publish lighweight containers with
ko
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.
- Part 1 — Getting started with Dapr on Kubernetes
- Part 2 — Building stateful applications with Dapr
- Part 3 — Creating and invoking Dapr-enabled services
- Part 4 — Creating event-based services with Dapr
References
- Blog post series GitHub repo github.com/vladimirvivien/dapr-examples
- The Distributed Application Runtime (Dapr)