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

Vladimir Vivien
6 min readJun 19, 2024

Creating and invoking Dapr-enabled services

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

The previous installment of this series (part 2) explored how to use the Dapr state management API to create a stateful service called frontendsvc. In this post, the third part of the series, we are going to extend the previous example to see how to create a service that can be invoked from the frontend service using the Dapr API.

The Genid service

In the previous version of our frontend service, the order ID was generated within the same service call. This version extracts that functionality and places it in a new stand-alone service called genid.

Fronend service invokes Genid service to generate order IDs

Now, when an order request is received, the frontend service application will invoke the genidsvc service using the Dapr Service Invocation API to generate a UUID value that can subsequently be used as the order id.

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

Pre-requisites

This example uses the same setup and resources that were configured in the part 2 of the series.

  • For detail walkthrough of how to setup your environment for the series, refer to part 1 and part 2 .

The Genid service source code

The code for the Genid service uses the Dapr API to create an HTTP service endpoint. But first, let’s define some package-level variables needed to configure the service:

var (
appPort = os.Getenv("APP_PORT") // app listening port
)

In the next code snippet, the code first declares a new service using the Dapr http service package. Then it attaches function geneateId (defined below) as a HTTP service endpoint handler for path /genid. Lastly, the newly instantiated service is started to listen on the the specified port.

import (
daprd "github.com/dapr/go-sdk/service/http"
)
...
func main() {
// Create a new service instance
dapr := daprd.NewService(fmt.Sprintf(":%s", appPort))

// Add service endpoint /genid
if err := dapr.AddServiceInvocationHandler("/genid", generateId); err != nil {
log.Fatalf("genid: invocation handler setup: %v", err)
}

// start the service
if err := dapr.Start(); err != nil && err != http.ErrServerClosed {
log.Fatalf("genid start: %v", err)
}
}

The next code snippet defines service handler function generateId which uses type common.Content (from package github.com/dapr/go-sdk/service/common) to wrap the generated ID.

func generateId(ctx context.Context, in *common.InvocationEvent) (*common.Content, error) {
id := uuid.New()
out := &common.Content{
Data: []byte(id.String()),
ContentType: in.ContentType,
DataTypeURL: in.DataTypeURL,
}

return out, nil
}

Invoking the service

Previously, the code for the frontend service uses a local function to generate the order ID. The following code snippet updates the code to use method dapr.InvokeMethod, from the Dapr service invocation API, to invoke the remote genidsvc service (on path genid) that was defined earlier.

func postOrder(w http.ResponseWriter, r *http.Request) {
...
// invoke genidsvc service to generate order UUID
out, err := daprClient.InvokeMethod(r.Context(), "genidsvc", "genid", "get")
if err != nil {
http.Error(w, "unable to post order", http.StatusInternalServerError)
return
}
orderID := fmt.Sprintf("order-%s", string(out))
...
}

As you might imagine, internally the Dapr client constructs a standard HTTP “GET” request to retrieve the order ID. Dapr has a well-defined HTTP API for invoking remote methods. The previous code would generate the following HTTP request:

http://localhost:5050/v1.0/invoke/genidsvc/method/genid

You can read more about the Dapr service invokation API here.

Building the source code

Next, let’s use ko to compile and build the source code into OCI containers:

ko build --local -B ./frontendsvc
ko build --local -B ./genidsvc

Once the images are built, let’s load them to the local kind dapr-cluster:

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

Deploying the application

This example will use three manifests:

Let’s focus on the manifest for the genidsvc as the others are unchanged from before.

The deployment for the genidsvc service includes annotations for its Dapr sidecar. Note the dapr.io/app-port: "5050": this is to configure the sidecar so that Dapr runtime can reach the service on its listening port. The annotation app-port value matches the exposed port of the application's containerPort value as shown below.

kind: Deployment
metadata:
name: genidsvc
spec:
...
template:
metadata:
labels:
app: genidsvc
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "genidsvc"
dapr.io/app-port: "5050"
spec:
containers:
- name: genidsvc
image: ko.local/genidsvc:latest
ports:
- containerPort: 5050

Deploying the manifests

The next step deploys the application configuration to the Kubernetes cluster:

kubectl apply -f ./manifest

Next, let’s ensure the Dapr component is running:

dapr components -k

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

We also should ensure that the Kubernetes deployments succeeded for each 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

$> kubectl get deployments -l app=genidsvc -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
genidsvc 1/1 1 1 46s genidsvc ko.local/genidsvc:latest app=genidsvc

Finally, to be thorough and avoid surprises, lets make sure that each application have two containers running in their pods (one for the 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

$> kubectl get pods -l app=genidsvc
NAME READY STATUS RESTARTS AGE
genidsvc-74b5459ff-mbh6r 2/2 Running 0 2m9

Running the application

At this point, the services are ready to start receiving HTTP requests. To keep things simple, we’ll use Kubernetes port-forward to expose the container ports of the frontend app:

kubectl port-forward deployment/frontendsvc 8080

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

Next, let’s 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: Mon, 08 Apr 2024 13:01:31 GMT
Content-Length: 75

The resulting JSON payload shows the new order ID (with the UUID attached). The frontend service successfully invoked the genid service and generated the order ID as shown below.

{"order":"order-e4e6240e-62a4-496d-9e2f-b20f4f5bab0b", "status":"received"}

Lastly, let’s use endpoint http://localhost:8080/orders/order/{id} to retrieve the order using the order ID generated by the genid service:

curl -i  -H "Content-type: application/json" "http://localhost:8080/orders/order/order-e4e6240e-62a4-496d-9e2f-b20f4f5bab0b"

HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 08 Apr 2024 13:16:46 GMT
Content-Length: 91

{"ID":"order-e4e6240e-62a4-496d-9e2f-b20f4f5bab0b","Items":["automobile"],"Completed":true}

The frontend service returns a JSON object showing information about the order was captured in the previous step.

Next step

In the third part of this series, we walked through the steps of creating a Dapr-managed service and shows how to invoke that service using the Dapr client API. In the next post we will continue to refine our example and show how use the Dapr publish/subscribe component to create event-based services.

References

--

--