envoyのgrpc-jsonを試す

# 背景

gRPCを直接たたけないclientがいるので、HTTPリクエストを変換してrpcをinvokeするのを試す

https://www.envoyproxy.io/

# インストール

Mac:

```
brew install envoy
```

Other: https://www.envoyproxy.io/docs/envoy/latest/start/install#

# Version

```
envoy --version

envoy  version: c919bdec19d79e97f4f56e4095706f8e6a383f1c/1.22.2/Modified/RELEASE/BoringSSL
```

# [gRPC-JSON transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter.html#route-configs-for-transcoded-requests)

## 1. Protobuf作成

```protobuf
syntax = "proto3";

package helloworld;

option go_package = "github.com/nakamasato/envoy-training/grpc-json";

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello(HelloRequest) returns (HelloReply) {
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
```

## 2. bufでコード生成

```yaml:buf.yaml
version: v1
name: buf.build/nakamasato/grpc-json
```

```yaml:buf.gen.yaml
version: v1
plugins:
  - name: go
    out: .
    opt: paths=source_relative
  - name: go-grpc
    out: .
    opt: paths=source_relative,require_unimplemented_servers=false
```

```
buf generate
```

これで、`helloworld_grpc.pb.go` と `helloworld.pb.go` が生成される

## 3. main.go作成


```go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"

    helloworldpb "github.com/nakamasato/envoy-training/grpc-json/protos"
)

type server struct {
    helloworldpb.UnimplementedGreeterServer
}

func NewServer() *server {
    return &server{}
}

func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
    return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}

func main() {
    // Create a listener on TCP port
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalln("Failed to listen:", err)
    }

    // Create a gRPC server object
    s := grpc.NewServer()
    // Attach the Greeter service to the server
    helloworldpb.RegisterGreeterServer(s, &server{})
    // Serve gRPC Server
    log.Println("Serving gRPC on 0.0.0.0:50051")
    log.Fatal(s.Serve(lis))
}
```

## 4. gRPC server起動

```
go run main.go
```

## 5. grpcurlでコール

```
grpcurl -d '{"name": "naka"}' -import-path ./protos -proto protos/helloworld.proto -plaintext localhost:50051 helloworld.Greeter/SayHello
{
  "message": "naka world"
}
```

## 6. protoの更新

以下を追加: 

```protobuf
import "google/api/annotations.proto";
```

```protobuf
option (google.api.http) = {
  post: "/say"
  body: "*"
};
```

最終的なprotoはこちら

```protobuf:helloworld.proto
syntax = "proto3";

package helloworld;

import "google/api/annotations.proto";

option go_package = "github.com/nakamasato/envoy-training/grpc-json";

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello(HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post : "/say"
      body : "*"
    };
  }
}

// The request message containing the user's name.
message HelloRequest { string name = 1; }

// The response message containing the greetings
message HelloReply { string message = 1; }
```

## 7. bufでコード生成

```yaml:buf.yaml
version: v1
name: buf.build/nakamasato/grpc-json
deps:
  - buf.build/googleapis/googleapis
```

```
buf mod update
buf build --as-file-descriptor-set -o helloworld.pb
```

grpc-json transcoderがproto descriptorを必要とするのでoutputしておく

## 8. grpcurlとproto descriptorで確認

```
grpcurl -d '{"name": "naka"}' -protoset protos/helloworld.pb -plaintext localhost:50051 helloworld.Greeter/SayHello
```

## 9. envoy config準備

ほぼ[sample-example](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter.html#sample-envoy-configuration)を使用

```yaml
admin:
  address:
    socket_address: {address: 0.0.0.0, port_value: 9901}

static_resources:
  listeners:
  - name: listener1
    address:
      socket_address: {address: 0.0.0.0, port_value: 51051}
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: grpc_json
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              # NOTE: by default, matching happens based on the gRPC route, and not on the incoming request path.
              # Reference: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
              - match: {prefix: "/helloworld.Greeter"}
                route: {cluster: grpc, timeout: 60s}
          http_filters:
          - name: envoy.filters.http.grpc_json_transcoder
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
              proto_descriptor: "protos/helloworld.pb"
              services: ["helloworld.Greeter"]
              print_options:
                add_whitespace: true
                always_print_primitive_fields: true
                always_print_enums_as_ints: false
                preserve_proto_field_names: false
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
  - name: grpc
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    dns_lookup_family: V4_ONLY
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}
    load_assignment:
      cluster_name: grpc
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: localhost
                port_value: 50051
```

## 10. envoy起動


```
envoy -c envoy.yaml
```

## 11. HTTP requestを確認

```
curl -d '{"name": "naka"}' http://localhost:51051/say
{
 "message": "naka world"
}
```


# 参考

- https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter.html#route-configs-for-transcoded-requests
- https://github.com/nakamasato/envoy-training/tree/main/grpc-json <- コードはこちら
- https://www.envoyproxy.io/