I have been experimenting with gRPC for some time now. I wrote some articles to cover the basics like What is gRPC? SSL/TLS Auth in gRPC, and communication patterns used in gRPC. In these topics I went through some of the advantages of gRPC over traditional REST API for inter-service communication – especially in a distributed architecture which led me to wonder about how gRPC works in Kubernetes environment. The crux is – gRPC offers great performance using Protobuf and natively supports uni and bi directional streaming.
I used an analogy of calculator server and clients calling out arithmetic operations from this server using gRPC protocol, in all the previous blogs. In this blog, I take the same example to take the next step – deploying these services on K8s cluster to demonstrate how you can use gRPC in Kubernetes context. Specifically, in this post I will:
- Containerize the client and server applications using Docker
- Prepare Kubernetes deployment YAMLs for these services
- Prepare Kubernetes service YAML to expose the calculator server
- Make sure uni and bidirectional communication is enabled between client and server
Please note that there are multiple ways gRPC in Kubernetes is used like – in Ingress load balancers, service meshes, etc. In fact, K8s also uses gRPC to enable efficient communication between kubelet and CRI (Container Runtime Interface). This post does not aim to cover these complex patterns, and instead stick to the basic explanation of making your microservices running on K8s cluster use gRPC.
Note: You can access the code discussed in this blog post here.
Why is gRPC a great choice for microservices on Kubernetes?
There are several reasons for using gRPC in any distributed architecture. Kubernetes is a container orchestration platform, which is capable of managing thousands of instances of hundreds of microservices on many nodes. These instances communicate with each other over K8s Services which also offer load balancing and routing of traffic to appropriate deployments. gRPC creates highly performant interfaces to the functionality offered by these microservices. Calling a remote procedure using gRPC is almost like making another function call. Below are more details on each aspect why this is advantageous.
- Performance with Protocol Buffers: Protocol Buffers make it possible to drastically reduce the payload during the serialization process. Additionally, HTTP/2 supports multiplexing enabling bidirectional communication.
- Strongly-typed service definitions: Microservice may be developed in multiple programming languages. gRPC provides a fixed contract as far as their interfaces are concerned in the native language. This reduces errors and simplifies debugging.
The above two aspects provide multiple opportunities to improve overall agility of the system, while assisting cost optimization.
Containerizing the microservices
I haven’t changed the calculator server code much since the last blog, since it simply exposes the arithmetic functionality. This Dockerfile builds the calculator server image. When run, the server starts listening on port 50051. Yes, SSL/TLS auth part of it as described in this blog post.
For the client service, I want to simulate random periodic requests to the calculator server to consume this arithmetic functionality. The infinite for loop runs every 3 seconds and attempts to call a randomly selectedFunction exposed by the server.
Copied!for { // Randomly select a function randomIndex := rand.Intn(len(functions)) selectedFunction := functions[randomIndex] // Execute the selected function log.Printf("Executing function: %T", selectedFunction) selectedFunction(client) // Sleep for 3 seconds time.Sleep(3 * time.Second) }
Currently, the calculator server exposes the below 4 functions with corresponding communication patterns. More details.
- Add() – Unary communication
- GenerateNumbers() – Server streaming
- ComputeAverage() – Client streaming
- ProcessNumbers() – bidirectional streaming
Next we containerize the client application using the Dockerfile below to prepare it to be deployed on Kubernetes.
Copied!# Build stage FROM golang:1.22-alpine AS builder WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY client/ ./client/ COPY proto/ ./proto/ # Build the application RUN CGO_ENABLED=0 GOOS=linux go build -o client ./client # Final stage FROM alpine:latest WORKDIR /app # Copy the binary from builder COPY --from=builder /app/client . COPY certs/ ./certs/ # Run the binary CMD ["./client"]
Kubernetes Deployment YAML files
As seen from the diagram below, we need three main things to deploy the above application on Kubernetes:
- Server Deployment – to deploy the calculator server application
- Server Service – to expose server’s gRPC functionality as ClusterIP
- Client Deployment – to deploy instances of the client application
Server YAML
To deploy the server pod, create a K8s manifest file as shown below. It uses the grpc-server image built in the last section to create containers, and exposes the port 50051 where the gRPC service is running. Note that we have used labels which we will further use in the Service file to expose the calculator server on the internal network for other pods to consume the same.
Copied!apiVersion: apps/v1 kind: Deployment metadata: name: grpc-server spec: replicas: 1 selector: matchLabels: app: grpc-server template: metadata: labels: app: grpc-server spec: containers: - name: grpc-server image: letsdotech/grpc-server:latest ports: - containerPort: 50051
Client YAML
Similarly, we use the file below to run the client application. We have passed an environment variable named “SERVER_ADDRESS”, to make the containerized client application aware of the location of the gRPC-enabled calculator server.
Copied!apiVersion: apps/v1 kind: Deployment metadata: name: grpc-client spec: replicas: 1 selector: matchLabels: app: grpc-client template: metadata: labels: app: grpc-client spec: containers: - name: grpc-client image: letsdotech/grpc-client:latest env: - name: SERVER_ADDRESS value: "grpc-server-service:50051"
From the environment variable value, you should already know what the name of the calculator server’s service would be. We will create this service in the next section.
K8s Service for Calculator server
It is a simple ClusterIP type of service, which also acts as a load balancer in case of multiple server instances running on the same K8s cluster. The metadata.name property specifies the service name, which is how the calculator server will be identified on the K8s network. Note that the selector specifies the label (grpc-server) which we set in the manifest for the server. This is useful during scaling operations.
Copied!apiVersion: v1 kind: Service metadata: name: grpc-server-service spec: selector: app: grpc-server # Match this with your server deployment labels ports: - port: 50051 targetPort: 50051 type: ClusterIP
Running everything together
In this section, we will go ahead and “apply” all the manifest files we have created and observe the deployment. Using the kubectl apply command, we will create all the pods on the K8s cluster. Run below commands:
-
kubectl apply -f server-deployment.yaml
– to deploy the calculator server instance -
kubectl apply -f server-service.yaml
– to expose calculator server functionality for clients -
kubectl apply -f client-deployment.yaml
– to deploy client application
Make sure that everything is running fine using kubectl get all command, as seen from the output below. We can see that the 2 deployments, and corresponding replica sets and pods are created and running. The service which exposes the calculator server is also created and is in line with the environment variable we set in the client’s deployment YAML.
Copied!kubectl get all NAME READY STATUS RESTARTS AGE pod/grpc-client-c9b746db7-bczgh 1/1 Running 0 12s pod/grpc-server-5665c65684-kkftg 1/1 Running 0 32s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/grpc-server-service ClusterIP 10.109.98.106 <none> 50051/TCP 2d23h service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d23h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/grpc-client 1/1 1 1 12s deployment.apps/grpc-server 1/1 1 1 32s NAME DESIRED CURRENT READY AGE replicaset.apps/grpc-client-c9b746db7 1 1 1 12s replicaset.apps/grpc-server-5665c65684 1 1 1 32s
This also means that the client application is already making random requests to consume the calculator server’s functions. The GIF below shows the output logs of both client and server.
You can further scale up or down the application above by changing the replicas in the deployment YAMLs. The final K8s deployment would look like below.
Leave a Reply