Apache APISIX와 gRPC-Web 통합하기

Fei Han

January 25, 2022

Ecosystem

gRPC 웹 소개

원래 Google에서 개발한 gRPC는 HTTP/2 위에서 구현된 고성능 원격 프로시저 호출 프레임워크입니다. 그러나 브라우저가 HTTP/2를 직접 노출하지 않기 때문에 웹 애플리케이션은 gRPC를 직접 사용할 수 없습니다. gRPC 웹은 이 문제를 해결하기 위한 표준화된 프로토콜입니다.

첫 번째 gRPC-웹 구현은 2018년에 JavaScript 라이브러리로 출시되었으며, 이를 통해 웹 애플리케이션이 gRPC 서비스와 직접 통신할 수 있습니다. 원리는 HTTP/1.1 및 HTTP/2와 호환되는 종단 간 gRPC 파이프라인을 생성하는 것입니다. 그런 다음 브라우저가 일반 HTTP 요청을 보내고, 브라우저와 서버 사이에 위치한 gRPC-웹 프록시가 요청과 응답을 변환합니다. gRPC와 유사하게, gRPC 웹은 웹 클라이언트와 백엔드 gRPC 서비스 간에 미리 정의된 계약을 사용합니다. Protocol Buffers는 메시지를 직렬화하고 인코딩하는 데 사용됩니다.

gRPC-웹 작동 방식

gRPC 웹을 사용하면 사용자는 브라우저나 Node 클라이언트를 사용하여 백엔드 gRPC 애플리케이션을 직접 호출할 수 있습니다. 그러나 브라우저 측에서 gRPC 서비스를 호출하기 위해 gRPC-웹을 사용하는 데는 몇 가지 제한 사항이 있습니다.

  • 클라이언트 측 스트리밍 및 양방향 스트리밍 호출은 지원되지 않습니다.
  • 도메인 간 gRPC 서비스 호출을 위해서는 서버 측에서 CORS를 구성해야 합니다.
  • gRPC 서버 측은 gRPC-웹을 지원하도록 구성되어야 하거나, 브라우저와 서버 간의 호출을 변환할 수 있는 타사 서비스 에이전트가 있어야 합니다.

Apache APISIX gRPC 웹 프록시

Apache APISIX는 플러그인을 통해 gRPC 웹 프로토콜의 프록시를 지원하며, grpc-web 플러그인에서 gRPC 웹과 gRPC 서버 간의 통신 시 프로토콜 변환 및 데이터 코덱 작업을 수행합니다. 그 통신 과정은 다음과 같습니다.

gRPC 웹 클라이언트 -> Apache APISIX (프로토콜 변환 & 데이터 코덱) -> gRPC 서버

다음은 gRPC 웹 클라이언트를 구축하고 Apache APISIX를 통해 gRPC 웹 요청을 프록시하는 방법을 보여주는 완전한 예제입니다. 아래 예제에서는 Go를 gRPC 서버 서버 핸들러로, Node를 gRPC 웹 클라이언트 요청자로 사용합니다.

Protocol Buffer 구성

첫 번째 단계는 Protocol Buffer 컴파일러 및 관련 플러그인을 설치하는 것입니다.

  1. protocproto-grpc-* 설치.

    Protocol Buffer 컴파일러 protoc.proto에 대한 Go, JavaScript 및 gRPC 웹 인터페이스 코드를 생성하기 위한 protoc-gen-goprotoc-gen-grpc-web 플러그인을 시스템에 설치해야 합니다.

    다음 스크립트를 실행하여 위 구성 요소를 설치하십시오.

    #!/usr/bin/env bash
    
    set -ex
    
    PROTOBUF_VERSION="3.19.0"
    wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    unzip protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    mv bin/protoc /usr/local/bin/protoc
    mv include/google /usr/local/include/
    chmod +x /usr/local/bin/protoc
    
    PROTO_GO_PLUGIN_VER="1.2.0"
    wget https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTO_GO_PLUGIN_VER}/protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    tar -zxvf protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    mv protoc-gen-go-grpc /usr/local/bin/protoc-gen-go
    chmod +x /usr/local/bin/protoc-gen-go
    
    PROTO_JS_PLUGIN_VER="1.3.0"
    wget https://github.com/grpc/grpc-web/releases/download/${PROTO_JS_PLUGIN_VER}/protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64
    mv protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64 /usr/local/bin/protoc-gen-grpc-web
    chmod +x /usr/local/bin/protoc-gen-grpc-web
    
  2. SayHello 예제 proto 파일 생성.

    // a6/echo.proto
    
    syntax = "proto3";
    
    package a6;
    
    option go_package = "./;a6";
    
    message EchoRequest {
    string message = 1;
    }
    
    message EchoResponse {
    string message = 1;
    }
    
    service EchoService {
    rpc Echo(EchoRequest) returns (EchoResponse);
    }
    

서버 측 애플리케이션 구성

  1. 서버 측 Go 원시 메시지 및 서비스/클라이언트 스텁 생성.

    protoc -I./a6 echo.proto --go_out=plugins=grpc:./a6
    
  2. 서버 측 핸들러 인터페이스 구현.

    // a6/echo.impl.go
    
    package a6
    
    import (
    "errors"
    "golang.org/x/net/context"
    )
    
    type EchoServiceImpl struct {
    }
    
    func (esi *EchoServiceImpl) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) {
    if len(in.Message) <= 0 {
        return nil, errors.New("message invalid")
    }
    return &EchoResponse{Message: "response: " + in.Message}, nil
    }
    
  3. 서버 측 애플리케이션 런타임 진입 파일.

    // server.go
    package main
    
    import (
    "fmt"
    "log"
    "net"
    
    "apisix.apache.org/example/a6"
    "google.golang.org/grpc"
    )
    
    func main() {
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50001))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    
    grpcServer := grpc.NewServer()
    a6.RegisterEchoServiceServer(grpcServer, &a6.EchoServiceImpl{})
    
    if err = grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %s", err)
    }
    }
    
  4. 서버 측 서비스 컴파일 및 시작.

    go build -o grpc-server server.go
    ./grpc-server
    

클라이언트 프로그램 구성

  1. 클라이언트 측 proto 코드 생성.

    클라이언트 측 JavaScript 원시 메시지, 서비스/클라이언트 스텁 및 gRPC 웹의 JavaScript 인터페이스 코드를 생성합니다.

    gRPC 웹의 proto 플러그인은 두 가지 모드의 코드 생성을 제공합니다.

    1. mode=grpcwebtext: 기본 생성 코드는 grpc-web-text 형식으로 페이로드를 전송합니다.

    • Content-type: application/grpc-web-text

    • 페이로드는 base64 인코딩 사용

    • 단일 및 서버 스트리밍 호출 지원

    1. mode=grpcweb: 페이로드를 바이너리 protobuf 형식으로 전송합니다.

    • Content-type: application/grpc-web+proto
    • 페이로드는 바이너리 protobuf 형식
    • 현재는 단일 호출만 지원
    protoc -I=./a6 echo.proto --js_out=import_style=commonjs:./a6 --grpc-web_out=import_style=commonjs,mode=grpcweb:./a6
    
  2. 클라이언트 측 종속성 설치.

    npm i grpc-web
    npm i google-protobuf
    
  3. 클라이언트 측 진입 파일 실행.

    // client.js
    const {EchoRequest} = require('./a6/echo_pb');
    const {EchoServiceClient} = require('./a6/echo_grpc_web_pb');
    // Apache APISIX의 진입점에 연결
    let echoService = new EchoServiceClient('http://127.0.0.1:9080');
    
    let request = new EchoRequest();
    request.setMessage("hello")
    
    echoService.echo(request, {}, function (err, response) {
       if (err) {
            console.log(err.code);
            console.log(err.message);
        } else {
            console.log(response.getMessage());
        }
    });
    
  4. 최종 프로젝트 구조

    $ tree .
    ├── a6
    │   ├── echo.impl.go
    │   ├── echo.pb.go
    │   ├── echo.proto
    │   ├── echo_grpc_web_pb.js
    │   └── echo_pb.js
    ├── client.js
    ├── server.go
    ├── go.mod
    ├── go.sum
    ├── package.json
    └── package-lock.json
    

위 단계를 완료하면 gRPC 서버 서버 애플리케이션과 gRPC 웹 클라이언트 애플리케이션을 구성하고, 서버 애플리케이션을 시작하여 50001 포트에서 요청을 수신할 준비가 되었습니다.

Apache APISIX 구성

다음으로, Apache APISIX 라우팅 플러그인 구성에서 grpc-web 플러그인을 활성화하여 gRPC 웹 요청을 프록시합니다.

  1. grpc-web 프록시 플러그인 활성화.

    라우팅은 접두사 일치를 사용해야 합니다 (예: /* 또는 /grpc/example/*), 왜냐하면 gRPC 웹 클라이언트는 URI에 proto에 선언된 패키지 이름, 서비스 인터페이스 이름, 메서드 이름 등을 전달하기 때문입니다 (예: /path/a6.EchoService/Echo), 절대 일치를 사용하면 플러그인이 URI에서 proto 정보를 추출할 수 없습니다.

    $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
        "uri":"/*", // 접두사 일치 모드
        "plugins":{
            "grpc-web":{} // gRPC 웹 플러그인 활성화
        },
        "upstream":{
            "scheme":"grpc",
            "type":"roundrobin",
            "nodes":{
                "127.0.0.1:50001":1 // gRPC 서버 수신 주소 및 포트
            }
        }
    }'
    
  2. gRPC 웹 프록시 요청 검증.

    Node에서 client.js를 실행하여 gRPC 웹 프로토콜 요청을 Apache APISIX로 보낼 수 있습니다.

    위의 클라이언트 측 및 서버 측 처리 로직은 각각 다음과 같습니다: 클라이언트가 서버에 hello라는 메시지를 보내고, 서버가 메시지를 받아 response: hello로 응답합니다. 실행 결과는 다음과 같습니다.

    $ node client.js
    response: hello
    
  3. grpc-web 프록시 플러그인 비활성화.

    라우팅 플러그인 구성에서 grpc-web 속성을 제거하기만 하면 됩니다.

    $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
        "uri":"/*",
        "plugins":{
        },
        "upstream":{
            "scheme":"grpc",
            "type":"roundrobin",
            "nodes":{
                "127.0.0.1:50001":1
            }
        }
    }'
    

요약

이 글은 Apache APISIX에서 grpc-web을 사용하는 방법에 대한 실습 경험을 제공합니다.

GitHub Discussions에서 토론을 시작하거나 메일링 리스트를 통해 소통해 보세요.

Tags: