Exposing a gRPC application using the Traefik ingress controller

Most ingress controllers break on gRPC — because HTTP/2 isn’t an afterthought. Learn how to expose gRPC services through Traefik on Civo Kubernetes, the right way.

5 minutes reading time

Written by

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

gRPC is often designed for service-to-service traffic inside a cluster, but there are plenty of reasons to reach one from outside it. You might have external clients, a partner integration, or another cluster in a different cloud that needs to call your service directly.

gRPC is a high-performance RPC framework from Google that runs over HTTP/2 and encodes its messages with Protocol Buffers. That HTTP/2 transport is exactly why you cannot just point an ordinary ingress rule at a gRPC service and expect it to work. The proxy in front of it has to speak HTTP/2 to the backend, and most controllers assume plain HTTP/1.1 unless you tell them otherwise.

This tutorial exposes a gRPC service through Traefik, a fast and lightweight ingress controller you can run on Civo Kubernetes. Traefik can speak HTTP/2 cleartext (h2c) to a gRPC backend, and it can terminate TLS at the edge.

Prerequisites

  • A Civo account
  • The Civo CLI installed and authenticated (civo apikey save <name> <key>)
  • kubectl and helm installed and on your PATH
  • grpcurl installed, to call the service once it is exposed
  • Familiarity with Kubernetes Deployments, Services, and Ingress

Launching a cluster using the Civo CLI

Create a two-node cluster to work in:

civo k3s create --create-firewall --nodes 2 --size g4s.kube.medium \
--save --switch --wait grpc-demo

The --save --switch flags merge the new cluster into your kubeconfig and make it the current context. Confirm the nodes are ready:

kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3s-grpc-demo-xxxx-node-pool-yyyy-aaaaa Ready <none> 2m v1.35.0+k3s1
k3s-grpc-demo-xxxx-node-pool-yyyy-bbbbb Ready <none> 2m v1.35.0+k3s1

Installing Traefik

Install Traefik with Helm and ask for a Civo Load Balancer so it gets a public IP:

helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik \
--namespace traefik --create-namespace \
--set service.type=LoadBalancer \
--wait

Wait for Civo to attach a public IP to the Traefik Service:

kubectl -n traefik get svc traefik
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.43.14.41 212.2.240.191 80:30853/TCP,443:30211/TCP 2m

Deploying a gRPC application

We'll use grpcbin, a small public gRPC server that exposes a handful of test methods and, importantly, has server reflection turned on. Reflection lets grpcurl discover the available methods without you having to supply the .proto files by hand, which makes verifying the setup much easier.

Deploy it, exposing the plaintext gRPC port 9000:

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpcbin
spec:
replicas: 2
selector:
matchLabels:
app: grpcbin
template:
metadata:
labels:
app: grpcbin
spec:
containers:
- name: grpcbin
image: moul/grpcbin
ports:
- containerPort: 9000
---
apiVersion: v1
kind: Service
metadata:
name: grpcbin
spec:
selector:
app: grpcbin
ports:
- name: grpc
port: 9000
targetPort: 9000
EOF

Check that both replicas are running:

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: randrpc-server-deployment
spec:
replicas: 2
selector:
matchLabels:
app: randrpc-server
template:
metadata:
labels:
app: randrpc-server
spec:
containers:
- name: randrpc-server
image: ghcr.io/s1ntaxe770r/randrpc-server:v1.5
ports:
- containerPort: 7070
---
apiVersion: v1
kind: Service
metadata:
name: randrpc-server-svc
spec:
selector:
app: randrpc-server
port:
- port: 80
targetPort: 7070
type: ClusterIP

Two grpcbin replicas, ready to answer gRPC calls.

Exposing the service over TLS

Civo gives your ingress a public IP through a Civo Load Balancer. We'll build a nip.io hostname from that IP so you don't need to own a domain. Grab the IP that Traefik is serving on:

LB_IP=$(kubectl -n traefik get svc traefik \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
HOST=grpc.${LB_IP}.nip.io
echo "Using HOST=$HOST"

Now create an IngressRoute on the secure entrypoint. The one field that makes this a gRPC route rather than an ordinary HTTP one is scheme: h2c on the service, which tells Traefik to speak HTTP/2 cleartext to the backend. The empty tls: {} block tells Traefik to terminate TLS on this route, using its built-in certificate for now:

kubectl apply -f - <<EOF
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grpcbin
spec:
entryPoints:
- websecure
routes:
- match: Host(\`${HOST}\`)
kind: Rule
services:
- name: grpcbin
port: 9000
scheme: h2c
tls: {}
EOF

Calling the gRPC service

Traefik is now terminating TLS with its own built-in certificate, which is not signed by a public authority, so we pass grpcurl the -insecure flag to skip certificate verification for this test. (The next section covers swapping in a trusted certificate so you can drop the flag.)

With reflection enabled on grpcbin, grpcurl can list the services the server exposes. Point it at your hostname on port 443:

grpcurl -insecure ${HOST}:443 list
addsvc.Add
grpc.gateway.examples.examplepb.ABitOfEverythingService
grpc.reflection.v1alpha.ServerReflection
grpcbin.GRPCBin
hello.HelloService

Now call one of the methods. The hello.HelloService/SayHello method takes a greeting and echoes it back:

grpcurl -insecure -d '{"greeting": "Civo"}' ${HOST}:443 hello.HelloService.SayHello
{
"reply": "hello Civo"
}

Reflection lists every service on the backend, and SayHello returns through Traefik over TLS and h2c.

Your request travelled from the client, over HTTP/2 with TLS to Traefik, and on to the gRPC backend as h2c, exactly the path a real external client would take.

Serving a trusted certificate with Let's Encrypt

The -insecure flag is fine for a quick test, but real clients expect a certificate signed by a public authority. When you have a domain of your own, Traefik can fetch one from Let's Encrypt automatically.

First, add a certificate resolver to the Traefik install. This tells Traefik to prove domain ownership over a TLS handshake on port 443 (the TLS-ALPN challenge), which needs nothing beyond the Load Balancer you already have:

helm upgrade traefik traefik/traefik \
--namespace traefik --reuse-values \
--set persistence.enabled=true \
--set persistence.size=128Mi \
--set podSecurityContext.fsGroup=65532 \
--set certificatesResolvers.le.acme.email=you@example.com \
--set certificatesResolvers.le.acme.storage=/data/acme.json \
--set certificatesResolvers.le.acme.tlsChallenge=true \
--wait

The persistence and fsGroup settings matter: Traefik stores the certificate in a file at /data/acme.json, and it runs as a non-root user against a read-only root filesystem, so it needs a writable volume it owns. Without the fsGroup, Traefik cannot write acme.json and the resolver is skipped.

Point an A record for your domain (for example grpc.example.com) at the Load Balancer's IP, then reference the resolver from the route:

HOST=grpc.example.com
kubectl apply -f - <<EOF
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grpcbin
spec:
entryPoints:
- websecure
routes:
- match: Host(\`${HOST}\`)
kind: Rule
services:
- name: grpcbin
port: 9000
scheme: h2c
tls:
certResolver: le
EOF

Traefik requests the certificate the first time the hostname is hit. Give it up to a minute, then call the service without -insecure:

grpcurl ${HOST}:443 list

On nip.io hostnames: Let's Encrypt validation can fail against shared wildcard-DNS services like nip.io, so the trusted-certificate path is worth doing on a domain you control. For a quick nip.io demo, stay with Traefik's built-in certificate and grpcurl -insecure as shown above.

Cleaning up

Delete the cluster when you are done so it stops billing:

civo k3s remove grpc-demo

Conclusion

You have exposed a gRPC service to the outside world through Traefik and terminated TLS at the edge, without installing a service mesh or cert-manager. The core of it was one IngressRoute with an h2c backend scheme, and swapping the built-in certificate for a Let's Encrypt one is a matter of adding a resolver when you have a domain to point at it.

From here you might look at A/B testing between two service versions with Traefik, or at adding authentication middleware in front of your gRPC route.

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Jubril Oyetunji is a DevOps engineer and technical writer with a strong focus on cloud-native technologies and open-source tools. His work centers on creating practical tutorials that help developers better understand platforms such as Kubernetes, NGINX, Rust, and Go.

As a contract technical writer, Jubril authored an extensive library of technical guides covering cloud-native infrastructure and modern development workflows. Many of his tutorials achieved strong search rankings, helping developers around the world learn and adopt emerging technologies.

View author profile