grpcurl icon indicating copy to clipboard operation
grpcurl copied to clipboard

How to access Kubernetes GRPC Ingress calls with prefix?

Open GautamSinghania opened this issue 3 years ago • 14 comments

We are using GRPC Ingress with Kubernetes and multiple paths. Here is the yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-grpc
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/backend-protocol: GRPC
    nginx.ingress.kubernetes.io/proxy-body-size: 64m
    nginx.ingress.kubernetes.io/ssl-redirect: 'true'
    nginx.ingress.kubernetes.io/ssl-passthrough: 'true'
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
  - host: xxx.xxx.internal
    http:
      paths:
      - path: /(.*)
        pathType: Prefix
        backend:
          service:
            name: service-1
            port:
              number: 5001
      - path: /prefix-2/(.*)
        pathType: Prefix
        backend:
          service:
            name: service-2
            port:
              number: 5001
  tls:
  - secretName: grpc-tls
    hosts:
    - xxx.xxx.internal

We have the proto file for this, and tried hitting the services using

grpcurl -d '<<DATA>>' -proto <<PROTO_FILE>> -insecure xxx.xxx.internal:443 tensorflow.serving.PredictionService.Predict

We are able to hit service-1, which does not have any prefix. However, we are not able to hit service-2. We tried using xxx.xxx.internal:443/test and /test/tensorflow.serving.PredictionService.Predict, but none of them work.

We know that there is a Go script that can access this. Can you do a similar thing for grpcurl?

GautamSinghania avatar Nov 10 '22 06:11 GautamSinghania

@GautamSinghania, does this mean the URLs for service-2 require a path prefix of /prefix-2/? That is generally not valid in gRPC.

The protocol spec for gRPC explicitly states that the path should be like so:

"/" Service-Name "/" {method name}

For this reason, none of the official gRPC clients will be able to access the service you are configuring since base paths are not supported. What kind of client are you using where you can make use of endpoints like that?

jhump avatar Nov 10 '22 12:11 jhump

We were using Go. I have asked my colleague working on this to create a script for testing purposes, will share that here once complete.

GautamSinghania avatar Nov 10 '22 13:11 GautamSinghania

As a standards question, what would be your suggestion to using multiple services under the same Ingress endpoint?

GautamSinghania avatar Nov 10 '22 14:11 GautamSinghania

what would be your suggestion to using multiple services under the same Ingress endpoint?

Two ways

  1. The simplest is to dispatch to one or the other based on the fully-qualified service name. So one service's ingress could enumerate all of the RPC services it exposes. A different service has an ingress that lists different service names. Obviously, having two services implement the same RPC service name will not work. (Though this can be problematic for other reasons, and is usually easily avoided.)
  2. Use vhosts/sub-domains. This allows for multiple services exposing the same service name. This is how Google Cloud works, where multiple sub-domains of googleapis.com (firebase, datastore, bigtable, appengine, etc) all implement the long-running operations service.

jhump avatar Nov 10 '22 15:11 jhump

@GautamSinghania, does this mean the URLs for service-2 require a path prefix of /prefix-2/? That is generally not valid in gRPC.

The protocol spec for gRPC explicitly states that the path should be like so:

"/" Service-Name "/" {method name}

For this reason, none of the official gRPC clients will be able to access the service you are configuring since base paths are not supported. What kind of client are you using where you can make use of endpoints like that?

I face the same problem - I do have a .NET 6 application (server and client), and the customer demands to have it in https://somehostname/<app>/ - would be the prefix. From what I understand here, it would be the "Request matching based on Prefix" A28

I did not follow gRPC development until now, so I do not know how this RFCs are applied. But from what I understand, it should be supported in the official clients.

It is my understanding, that those gRPC (A27 and A28) are talking about the SERVER SIDE - something about envoy proxy, and generic configuration. The crucial point, to me, is that this is a common use case, and those things happen in real life.

thunder7553 avatar Nov 22 '22 10:11 thunder7553

FWIW, how to access a GRPC service hosted in a directory in .NET 6 is documented on learn.microsoft.com

thunder7553 avatar Nov 22 '22 10:11 thunder7553

would be the prefix. From what I understand here, it would be the "Request matching based on Prefix" A28

@thunder7553, that proposal is for configuring XDS clients to support path matching for routing. But that does not mention anything about custom path prefixes. The gRPC Go client still has no option for specifying a base URI path, and the HTTP/2 wire spec still has no mention of support for a custom path prefix. FWIW, they are not server-side: XDS is a protocol for client-side load balancing, so it configures how clients should route requests to different discovered backends.

FWIW, how to access a GRPC service hosted in a directory in .NET 6 is documented on learn.microsoft.com

Note the big warning at the top of that page.

jhump avatar Nov 22 '22 16:11 jhump

Thanks for the info, you are right indeed. I have to dig into that.

thunder7553 avatar Nov 24 '22 09:11 thunder7553

@thunder7553 This is what worked for me:

apiVersion: v1
kind: Service
metadata:
  name: "places"
  labels:
    app: "places"
    monitor: "true"
spec:
  type: NodePort
  ports:
    - name: tcp
      protocol: TCP
      port: 80
      targetPort: 50051
  selector:
    app: "places"

--
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
  name: "tracker"
spec:
  ingressClassName: nginx
  rules:
  - host: "dev.something.com"
    http:
      paths:
      - path: /org.places.v1.PlacesService # use package.ServiceName
        pathType: Prefix
        backend:
          service:
            name: "dev"
            port:
              number: 80
  tls:
    - hosts:
        - "dev.something.com"
      secretName: "dev.something.com-tls"

Then this will work:

grpcurl -import-path path/to/proto -proto service.proto -v -d '{"id":"018bd0c9-1ef2-788b-945f-a437e28b2a1c"}' \
    dev.something.com:443 org.places.v1.PlacesService/GetPlace

I have not managed to make the reflection work. This is because nginx gets /grpc.reflection.v1.ServerReflection/ServerReflectionInfo as the path. If we were able to get /org.places.v1.PlacesService/grpc.reflection.v1.ServerReflection/ServerReflectionInfo it would work since then nginx would know what path to take.

This is what you would see in the ingress logs:

111.77.196.166 - - [07/Jan/2024:05:37:51 +0000] "POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo HTTP/2.0" 499 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 165 10.715 [upstream-default-backend] [] - - - - 2fe2729a167c69404b3707d59f658e2a <---- if you attempt reflection, it fails
111.77.196.166 - - [07/Jan/2024:05:39:22 +0000] "POST /org.places.v1.PlacesService/GetPlace HTTP/2.0" 200 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 159 0.005 [development-places-80] [] 10.64.0.22:50051 0 0.006 200 f3a2aaa701016d21b246498657e1b87e
111.77.196.166 - - [07/Jan/2024:05:40:06 +0000] "POST /org.places.v1.PlacesService/GetPlace HTTP/2.0" 200 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 159 0.010 [development-places-80] [] 10.64.0.22:50051 0 0.010 200 71eef1e2546b09db6139bd6c7a67102f

CC @jhump

renevall avatar Jan 07 '24 05:01 renevall

@thunder7553 This is what worked for me:

apiVersion: v1
kind: Service
metadata:
  name: "places"
  labels:
    app: "places"
    monitor: "true"
spec:
  type: NodePort
  ports:
    - name: tcp
      protocol: TCP
      port: 80
      targetPort: 50051
  selector:
    app: "places"

--
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
  name: "tracker"
spec:
  ingressClassName: nginx
  rules:
  - host: "dev.something.com"
    http:
      paths:
      - path: /org.places.v1.PlacesService # use package.ServiceName
        pathType: Prefix
        backend:
          service:
            name: "dev"
            port:
              number: 80
  tls:
    - hosts:
        - "dev.something.com"
      secretName: "dev.something.com-tls"

Then this will work:

grpcurl -import-path path/to/proto -proto service.proto -v -d '{"id":"018bd0c9-1ef2-788b-945f-a437e28b2a1c"}' \
    dev.something.com:443 org.places.v1.PlacesService/GetPlace

I have not managed to make the reflection work. This is because nginx gets /grpc.reflection.v1.ServerReflection/ServerReflectionInfo as the path. If we were able to get /org.places.v1.PlacesService/grpc.reflection.v1.ServerReflection/ServerReflectionInfo it would work since then nginx would know what path to take.

This is what you would see in the ingress logs:

111.77.196.166 - - [07/Jan/2024:05:37:51 +0000] "POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo HTTP/2.0" 499 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 165 10.715 [upstream-default-backend] [] - - - - 2fe2729a167c69404b3707d59f658e2a <---- if you attempt reflection, it fails
111.77.196.166 - - [07/Jan/2024:05:39:22 +0000] "POST /org.places.v1.PlacesService/GetPlace HTTP/2.0" 200 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 159 0.005 [development-places-80] [] 10.64.0.22:50051 0 0.006 200 f3a2aaa701016d21b246498657e1b87e
111.77.196.166 - - [07/Jan/2024:05:40:06 +0000] "POST /org.places.v1.PlacesService/GetPlace HTTP/2.0" 200 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 159 0.010 [development-places-80] [] 10.64.0.22:50051 0 0.010 200 71eef1e2546b09db6139bd6c7a67102f

CC @jhump

I my both client and server are in .NET. I have the same issue when exposing the server through headless service. Does the solution that you offered works for headless service also?

VenkateshSrini avatar Jan 08 '24 01:01 VenkateshSrini

This is because nginx gets /grpc.reflection.v1.ServerReflection/ServerReflectionInfo as the path. If we were able to get /org.places.v1.PlacesService/grpc.reflection.v1.ServerReflection/ServerReflectionInfo it would work since then nginx would know what path to take.

But then the server would have no idea how to handle it. I'm afraid the gRPC protocol does not describe any support for custom path prefixes. The recommendation is usually to route by service name (as you are doing) or to use sub-domains if you need to route the same service name to multiple backends.

jhump avatar Jan 08 '24 15:01 jhump

@jhump

Is it documented somewhere that gRPC protocol does not support custom path prefixes?

I am also facing the problem where if i define a custom path (instead of /, i use /atif/app1/), it does not work. Only if i just use path: /, the requests reaches server and i get a response.

Regards!

atifhafeez avatar Jan 20 '24 09:01 atifhafeez

@atifhafeez, the gRPC protocol spec makes it pretty clear that the path must /service-name/method-name: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests

Currently, grpcurl is built on top of the official gRPC Go implementation, which does not provide the ability to send requests with custom URL path prefixes. In fact, if you look at all of the official (Google-provided) gRPC client libraries, none of them allow that. So making such a change to grpcurl would, unfortunately, require replacing or re-writing the transport implementation, which is a non-trivial lift.

jhump avatar Jan 23 '24 15:01 jhump

@thunder7553 This is what worked for me:

apiVersion: v1
kind: Service
metadata:
  name: "places"
  labels:
    app: "places"
    monitor: "true"
spec:
  type: NodePort
  ports:
    - name: tcp
      protocol: TCP
      port: 80
      targetPort: 50051
  selector:
    app: "places"

--
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
  name: "tracker"
spec:
  ingressClassName: nginx
  rules:
  - host: "dev.something.com"
    http:
      paths:
      - path: /org.places.v1.PlacesService # use package.ServiceName
        pathType: Prefix
        backend:
          service:
            name: "dev"
            port:
              number: 80
  tls:
    - hosts:
        - "dev.something.com"
      secretName: "dev.something.com-tls"

Then this will work:

grpcurl -import-path path/to/proto -proto service.proto -v -d '{"id":"018bd0c9-1ef2-788b-945f-a437e28b2a1c"}' \
    dev.something.com:443 org.places.v1.PlacesService/GetPlace

I have not managed to make the reflection work. This is because nginx gets /grpc.reflection.v1.ServerReflection/ServerReflectionInfo as the path. If we were able to get /org.places.v1.PlacesService/grpc.reflection.v1.ServerReflection/ServerReflectionInfo it would work since then nginx would know what path to take. This is what you would see in the ingress logs:

111.77.196.166 - - [07/Jan/2024:05:37:51 +0000] "POST /grpc.reflection.v1.ServerReflection/ServerReflectionInfo HTTP/2.0" 499 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 165 10.715 [upstream-default-backend] [] - - - - 2fe2729a167c69404b3707d59f658e2a <---- if you attempt reflection, it fails
111.77.196.166 - - [07/Jan/2024:05:39:22 +0000] "POST /org.places.v1.PlacesService/GetPlace HTTP/2.0" 200 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 159 0.005 [development-places-80] [] 10.64.0.22:50051 0 0.006 200 f3a2aaa701016d21b246498657e1b87e
111.77.196.166 - - [07/Jan/2024:05:40:06 +0000] "POST /org.places.v1.PlacesService/GetPlace HTTP/2.0" 200 0 "-" "grpcurl/1.8.9 grpc-go/1.57.0" 159 0.010 [development-places-80] [] 10.64.0.22:50051 0 0.010 200 71eef1e2546b09db6139bd6c7a67102f

CC @jhump

I my both client and server are in .NET. I have the same issue when exposing the server through headless service. Does the solution that you offered works for headless service also?

Can you please suggest how I go about this for headless service

VenkateshSrini avatar Jan 27 '24 03:01 VenkateshSrini