NGINX API Gateway – gRPC API Gateway 배포

이번 모범사례는 NGINX API Gateway를 gRPC API Gateway로 배포하는 방법에 대해 살펴봅니다.

  • 1부에서는 NGINX RESTful, HTTP 기반 API용 API Gateway로 API를 정의하고 API 클이언트 인증을 구현하는 지침을 제공합니다.
  • 2부에서는 이러한 사용 사례를 확장하고 프로덕션 환경에서 Backend API 서비스를보호하기 위해 적용할 수 있는 다양한 보호 장치를 살펴봅니다.

마이크로서비스 애플리케이션의 핵심은 HTTP API입니다.

최신 애플리케이션을 위한 JSON 메시지 형식을 사용하는 REST API의 인기에도 불구하고 모든 시나리오 또는 모든 조직에 이상적인 접근 방식은 아닙니다. 가장 일반적인 문제는 다음과 같습니다.

  • 문서 표준  – 훌륭한 개발자 규율이나 의무화된 문서 요구 사항이 없으면 정확한 정의가 없는 여러 REST API로 끝나기가 너무 쉽습니다. Open API 사양은 REST API를 위한 일반 인터페이스 설명 언어로 등장했지만 사용은 선택 사항이며 개발 조직 내에서 강력한 거버넌스가 필요합니다.
  • 이벤트 및 long‑lived connections  – REST API 및 HTTP를 전송으로 사용하는 것은 모든 API 호출에 대한 요청-응답 패턴을 크게 좌우합니다. 애플리케이션에 서버 생성 이벤트가 필요한 경우 HTTP Long-Polling 및 WebSocket과 같은 솔루션을 사용하면 도움이 될 수 있지만 이러한 솔루션을 사용하려면 궁극적으로 별도의 인접 API를 빌드해야 합니다.
  • 복잡한 트랜잭션  – REST API는 각각 URI로 표시되는 고유한 리소스의 개념을 중심으로 구축됩니다. 애플리케이션 이벤트가 여러 리소스를 업데이트하도록 호출하면 비효율적인 여러 API 호출이 필요하거나 REST의 핵심 원칙과 모순되는 Backend에서 복잡한 트랜잭션을 구현해야 합니다.

최근 몇 년 동안 gRPC는 분산 애플리케이션, 특히 마이크로서비스 애플리케이션을 구축하기 위한 대안으로 부상했습니다. 원래 Google에서 개발한 gRPC는 2015년에 오픈소스로 제공되었으며 현재 Cloud Native Computing Foundation의 프로젝트입니다. 중요하게도 gRPC는 HTTP/2를 전송 메커니즘으로 사용하여 바이너리 데이터 형식과 다중화된 스트리밍 기능을 활용합니다.

gRPC의 주요 이점은 다음과 같습니다.

  • 밀접하게 결합된 인터페이스 정의 언어(프로토콜 버 )
  • 스트리밍 데이터에 대한 기본 지원(양방향)
  • 효율적인 바이너리 데이터 형식
  • 많은 프로그래밍 언어에 대한 자동화된 코드 생성으로 상호 운용성 문제 없이 진정한 다국어 개발 환경 가능

목차

1. gRPC API Gateway 정의
2. 샘플 gRPC 서비스 실행
  2-1. gRPC 요청 라우팅
  2-2. 정확한 라우팅
3. 오류에 응답하기
4. gRPC 메타데이터로 API 클라이언트 인증
5. gRPC Backend 헬스 체크 구현
6. 속도 제한 및 기타 API Gateway 제어(Control) 적용
7. 요약

1. gRPC API Gateway 정의

gRPC 트래픽의 기본 동작과 특성은 NGINX가 gRPC API Gateway로 배포될 때 동일한 접근 방식을 취하도록 합니다. 동일한 호스트 이름과 포트에서 HTTP 및 gRPC 트래픽을 모두 공유할 수 있지만 이를 분리하는 것이 더 나은 여러 가지 이유가 있습니다.

  • REST 및 gRPC 애플리케이션용 API 클라이언트는 다양한 형식의 오류 응답을 예상합니다.
  • 액세스 로그에 대한 관련 필드는 REST와 gRPC에 따라 다릅니다.
  • gRPC는 레거시 웹 브라우저를 절대 다루지 않기 때문에 더 엄격한 TLS 정책을 가질 수 있습니다.

이 분리를 달성하기 위해 우리는 gRPC API Gateway에 대한 구성을 /etc/nginx/conf.d 디렉토리에 있는 기본 gRPC 구성 파일인 grpc_gateway.conf의 자체 server{} 블록에 넣습니다.

log_format grpc_json escape=json '{"timestamp":"$time_iso8601",'
           '"client":"$remote_addr","uri":"$uri","http-status":$status,'
           '"grpc-status":$grpc_status,"upstream":"$upstream_addr"'
           '"rx-bytes":$request_length,"tx-bytes":$bytes_sent}';

map $upstream_trailer_grpc_status $grpc_status {
    default $upstream_trailer_grpc_status; # grpc-status is usually a trailer
    ''      $sent_http_grpc_status; # Else use the header, whatever its source
}

server {
    listen 50051 http2; # In production, comment out to disable plaintext port
    listen 443   http2 ssl;
    server_name  grpc.example.com;
    access_log   /var/log/nginx/grpc_log.json grpc_json;

    # TLS config
    ssl_certificate      /etc/ssl/certs/grpc.example.com.crt;
    ssl_certificate_key  /etc/ssl/private/grpc.example.com.key;
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  5m;
    ssl_ciphers          HIGH:!aNULL:!MD5;
    ssl_protocols        TLSv1.2 TLSv1.3;

gRPC 트래픽에 대한 액세스 로그의 항목 형식을 정의하는 것으로 시작합니다(1-4행). 이 예에서는 JSON 형식을 사용하여 각 요청에서 가장 관련성이 높은 데이터를 캡처합니다. 예를 들어, 모든 gRPC 요청은 POST를 사용하므로 HTTP 메서드는 포함되지 않습니다. 또한 HTTP 상태 코드와 함께 gRPC 상태 코드를 기록합니다. 그러나 gRPC 상태 코드는 다른 방식으로 생성할 수 있습니다. 정상적인 조건에서 grpc-status는 백엔드에서 HTTP/2 트레일러로 반환되지만 일부 오류 조건의 경우 백엔드 또는 NGINX 자체에서 HTTP/2 헤더로 반환될 수 있습니다. 액세스 로그를 단순화하기 위해 맵 블록(라인 6–9)을 사용하여 새 변수 $grpc_status를 평가하고 시작 위치에서 gRPC 상태를 얻습니다.

이 구성에는 일반 텍스트(포트 50051) 및 TLS 보호(포트 443) 트래픽을 모두 테스트할 수 있도록 두 개의 수신 지시문(12행 및 13행)이 포함되어 있습니다. http2 매개변수는 HTTP/2 연결을 허용하도록 NGINX를 구성합니다. 이것은 ssl 매개변수와 무관합니다. 또한 포트 50051은 gRPC에 대한 기존 일반 텍스트 포트이지만 프로덕션에서 사용하기에는 적합하지 않습니다.

LS 구성은 ssl_protocols 지시문(행 23)을 제외하고는 일반적입니다. 이 지시문은 TLS 1.2를 가장 취약한 허용 프로토콜로 지정합니다. HTTP/2 사양은 모든 클라이언트가 TLS에 대한 SNI(서버 이름 표시) 확장을 지원하도록 보장하는 TLS 1.2(또는 그 이상)의 사용을 의무화합니다. 이는 gRPC API Gateway가 포트 443을 다른 server{} 블록에 정의된 가상 서버와 공유할 수 있음을 의미합니다.

2. 샘플 gRPC 서비스 실행

NGINX의 gRPC 기능을 탐색하기 위해 여러 gRPC 서비스가 배포된 gRPC API Gateway의 주요 구성 요소를 나타내는 간단한 테스트 환경을 사용하고 있습니다. 공식 gRPC 가이드의 두 가지 샘플 애플리케이션인 helloworld(Go로 작성)와 RouteGuide(Python으로 작성)를 사용합니다. RouteGuide 애플리케이션은 4가지 gRPC 서비스 메서드가 각각 포함되어 있기 때문에 특히 유용합니다.

  • 단순 RPC(단일 요청-응답)
  • 응답 스트리밍 RPC
  • 요청 스트리밍 RPC
  • 양방향 스트리밍 RPC

두 gRPC 서비스는 모두 NGINX 호스트에 Docker 컨테이너로 설치됩니다.

NGINX API Gateway - gRPC API Gateway 배포
NGINX를 gRPC API Gateway로 사용하기 위한 테스트 환경

사용 가능한 컨테이너의 주소와 함께 RouteGuide 및 helloworld 서비스에 대해 알도록 NGINX를 구성합니다.

upstream routeguide_service {
    zone routeguide_service 64k;
    server 127.0.0.1:10001;
    server 127.0.0.1:10002;
    server 127.0.0.1:10003;
}

upstream helloworld_service {
    zone helloworld_service 64k;
    server 127.0.0.1:20001;
    server 127.0.0.1:20002;
}

각 gRPC 서비스에 대한 업스트림 블록(라인 40–45 및 47–51)을 추가하고 gRPC 서버 코드를 실행하는 개별 컨테이너의 주소로 채웁니다.

2-1. gRPC 요청 라우팅

gRPC(50051)에 대한 기존 일반 텍스트 포트에서 수신하는 NGINX를 사용하여 구성에 라우팅 정보를 추가하여 클라이언트 요청이 올바른 백엔드 서비스에 도달하도록 합니다. 그러나 먼저 gRPC 메서드 호출이 HTTP/2 요청으로 표현되는 방식을 이해해야 합니다. 다음 다이어그램은 NGINX에서 볼 수 있는 것처럼 패키지, 서비스 및 RPC 메서드가 URI를 형성하는 방법을 보여주는 RouteGuide 서비스용 route_guide.proto 파일의 축약된 버전을 보여줍니다.

프로토콜 버퍼 RPC 메서드가 HTTP/2 요청으로 변환되는 방법
프로토콜 버퍼 RPC 메서드가 HTTP/2 요청으로 변환되는 방법

따라서 HTTP/2 요청에 포함된 정보는 단순히 패키지 이름(여기서는 routeguide 또는 helloworld)을 일치시켜 라우팅 목적으로 사용할 수 있습니다.

    # Routing
    location /routeguide. {
        grpc_pass grpc://routeguide_service;
    }
    location /helloworld. {
        grpc_pass grpc://helloworld_service;
    }

수정자가 없는 첫 번째 위치 블록(26행)은 /routeguide와 같은 접두사 일치를 정의합니다. 해당 패키지의 해당 .proto 파일에 정의된 모든 서비스 및 RPC 메서드와 일치합니다. 따라서 grpc_pass 지시문(27행)은 RouteGuide 클라이언트의 모든 요청을 업스트림 그룹 routeguide_service로 전달합니다. 이 구성(및 29행과 30행의 helloworld 서비스에 대한 병렬 구성)은 gRPC 패키지와 해당 백엔드 서비스 간의 간단한 매핑을 제공합니다.

grpc_pass 지시문에 대한 인수는 일반 텍스트 gRPC 연결을 사용하여 요청을 프록시하는 grpc:// 체계로 시작합니다. 백엔드가 TLS용으로 구성된 경우 grpcs:// 체계를 사용하여 종단 간 암호화로 gRPC 연결을 보호할 수 있습니다.

RouteGuide 클라이언트를 실행한 후 로그 파일 항목을 검토하여 라우팅 동작을 확인할 수 있습니다. 여기에서 RouteChat RPC 메서드가 포트 10002에서 실행되는 컨테이너로 라우팅되었음을 알 수 있습니다.

$ python route_guide_client.py
...
$ tail -1 /var/log/nginx/grpc_log.json | jq
{
  "timestamp": "2021-01-20T12:17:56+01:00",
  "client": "127.0.0.1",
  "uri": "/routeguide.RouteGuide/RouteChat",
  "http-status": 200,
  "grpc-status": 0,
  "upstream": "127.0.0.1:10002",
  "rx-bytes": 161,
  "tx-bytes": 212
}

2-2. 정확한 라우팅

위에 표시된 것처럼 여러 gRPC 서비스를 다른 백엔드로 라우팅하는 것은 간단하고 효율적이며 구성 라인이 거의 필요하지 않습니다. 그러나 프로덕션 환경의 라우팅 요구 사항은 더 복잡할 수 있으며 URI의 다른 요소(gRPC 서비스 또는 개별 RPC 방법)를 기반으로 하는 라우팅이 필요할 수 있습니다.

다음 구성 스니펫은 양방향 스트리밍 RPC 메소드 RouteChat이 하나의 백엔드로 라우팅되고 다른 모든 RouteGuide 메소드가 다른 백엔드로 라우팅되도록 이전 예제를 확장합니다.

# Service-level routing
location /routeguide.RouteGuide/ {
    grpc_pass grpc://routeguide_service_default;
}

# Method-level routing
location = /routeguide.RouteGuide/RouteChat {
    grpc_pass grpc://routeguide_service_streaming;
}

두 번째 위치 지시문(7행)은 =(등호) 수정자를 사용하여 이것이 RouteChat RPC 메서드에 대한 URI와 정확히 일치함을 나타냅니다. 정확한 일치는 접두사 일치 전에 처리됩니다. 즉, RouteChat URI에 대해 다른 위치 블록이 고려되지 않습니다.

3. 오류에 응답하기

gRPC 오류는 기존 HTTP 트래픽의 오류와 약간 다릅니다. 클라이언트는 오류 조건이 gRPC 응답으로 표현되기를 기대하므로 NGINX가 gRPC API Gateway로 구성된 경우 기본 NGINX 오류 페이지(HTML 형식)가 적합하지 않습니다. gRPC 클라이언트에 대한 사용자 지정 오류 응답 집합을 지정하여 이 문제를 해결합니다.


    # Error responses
    include conf.d/errors.grpc_conf; # gRPC-compliant error responses
    default_type application/grpc;   # Ensure gRPC for all error responses

전체 gRPC 오류 응답 세트는 비교적 길고 대체로 정적인 구성이므로 별도의 파일 errors.grpc_conf에 보관하고 include 지시문(34행)을 사용하여 참조합니다. HTTP/REST 클라이언트와 달리 gRPC 클라이언트 애플리케이션은 광범위한 HTTP 상태 코드를 처리할 것으로 예상되지 않습니다. gRPC 문서는 클라이언트가 항상 적절한 응답을 받을 수 있도록 NGINX와 같은 중간 프록시가 HTTP 오류 코드를 gRPC 상태 코드로 변환해야 하는 방법을 지정합니다. 이 매핑을 수행하기 위해 error_page 지시문을 사용합니다.

# Standard HTTP-to-gRPC status code mappings
# Ref: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
#
error_page 400 = @grpc_internal;
error_page 401 = @grpc_unauthenticated;
error_page 403 = @grpc_permission_denied;
error_page 404 = @grpc_unimplemented;
error_page 429 = @grpc_unavailable;
error_page 502 = @grpc_unavailable;
error_page 503 = @grpc_unavailable;
error_page 504 = @grpc_unavailable;

각 표준 HTTP 상태 코드는 @ 접두사를 사용하여 명명된 위치에 전달되므로 gRPC 호환 응답이 생성될 수 있습니다. 예를 들어 HTTP 404 응답은 내부적으로 @grpc_unimplemented 위치로 리디렉션되며, 이 위치는 파일의 뒷부분에서 정의됩니다.

location @grpc_unimplemented {
    add_header grpc-status 12;
    add_header grpc-message unimplemented;
    return 204;
}

@grpc_unimplemented 명명된 위치는 내부 NGINX 처리에만 사용할 수 있습니다. 라우팅 가능한 URI가 없기 때문에 클라이언트는 이를 직접 요청할 수 없습니다. 이 위치 내에서 필수 gRPC 헤더를 채우고 HTTP 상태 코드 204(내용 없음)를 사용하여 응답 본문 없이 전송하여 gRPC 응답을 구성합니다.

curl(1) 명령을 사용하여 존재하지 않는 gRPC 메서드를 요청하는 잘못 동작하는 gRPC 클라이언트를 모방할 수 있습니다. 그러나 프로토콜 버퍼는 바이너리 데이터 형식을 사용하기 때문에 curl은 일반적으로 gRPC 테스트 클라이언트로 적합하지 않습니다. 명령줄에서 gRPC를 테스트하려면 grpc_cli 사용을 고려하십시오.

$ curl -i --http2 -H "Content-Type: application/grpc" -H "TE: trailers" -X POST https://grpc.example.com/does.Not/Exist
HTTP/2 204 
server: nginx/1.19.5
date: Wed, 20 Jan 2021 15:03:41 GMT
grpc-status: 12
grpc-message: unimplemented

위에서 참조한 grpc_errors.conf 파일에는 시간 초과 및 클라이언트 인증서 오류와 같이 NGINX가 생성할 수 있는 기타 오류 응답에 대한 HTTP-to-gRPC 상태 코드 매핑도 포함되어 있습니다.

4. gRPC 메타데이터로 API 클라이언트 인증

gRPC 메타데이터를 사용하면 클라이언트가 프로토콜 버퍼 사양( .proto 파일) 의 일부가 될 필요 없이 RPC 메서드 호출과 함께 추가 정보를 보낼 수 있습니다 . 메타데이터는 키-값 쌍의 간단한 목록이며 각 쌍은 별도의 HTTP/2 헤더로 전송됩니다. 따라서 메타데이터는 NGINX에 쉽게 액세스할 수 있습니다.

메타데이터의 많은 사용 사례 중에서 클라이언트 인증은 gRPC API Gateway에 가장 일반적입니다. 다음 구성 스니펫은 NGINX Plus가 gRPC 메타데이터를 사용하여 JWT 인증을 수행하는 방법을 보여줍니다(JWT 인증은 NGINX Plus 전용). 이 예에서 JWT는 인증 토큰 메타데이터로 전송됩니다.

location /routeguide. {
    auth_jwt realm=routeguide token=$http_auth_token;
    auth_jwt_key_file my_idp.jwk;
    grpc_pass grpc://routeguide_service;
}

모든 HTTP 요청 헤더는 $http_header라는 변수로 NGINX Plus에서 사용할 수 있습니다. 헤더 이름의 하이픈(-)은 변수 이름의 밑줄( _ )로 변환되므로 JWT는 $http_auth_token(2행)으로 사용할 수 있습니다.

API 키가 인증에 사용되는 경우(아마도 기존 HTTP/REST API와 함께) 이러한 키를 gRPC 메타데이터로 전달하고 NGINX에서 유효성을 검사할 수도 있습니다. API 키 인증을 위한 구성은 이 모범사례 시리즈의 1부에서 제공됩니다.

5. gRPC Backend 헬스 체크 구현

여러 Backend로 트래픽을 로드 밸런싱할 때 다운되었거나 사용할 수 없는 Backend로 요청을 보내는 것을 피하는 것이 중요합니다. NGINX Plus를 사용하면 활성 상태 확인을 사용하여 Backend에 대역 외 요청을 사전에 보내고 예상대로 상태 확인에 응답하지 않을 때 부하 분산 순환에서 해당 요청을 제거할 수 있습니다. 이러한 방식으로 클라이언트 요청이 서비스가 중단된 Backend에 도달하지 않도록 합니다.

다음 구성 스니펫은 RouteGuide 및 helloworld gRPC 서비스에 대한 활성 상태 확인을 활성화합니다. 관련 구성을 강조 표시하기 위해 이전 섹션에서 사용된 grpc_gateway.conf 파일에 포함된 일부 지시문을 생략합니다.

server {
    listen 50051 http2; # Plaintext

    # Routing
    location /routeguide. {
        grpc_pass grpc://routeguide_service;
        health_check type=grpc grpc_status=12; # 12=unimplemented
    }
    location /helloworld. {
        grpc_pass grpc://helloworld_service;
        health_check type=grpc grpc_status=12; # 12=unimplemented
    }
}

이제 각 경로에 대해 health_check 지시문도 지정합니다(17 및 21행). type=grpc 인수로 지정된 대로 NGINX Plus는 gRPC 상태 확인 프로토콜을 사용하여 업스트림 그룹의 모든 서버에 상태 확인을 보냅니다. 그러나 간단한 gRPC 서비스는 gRPC 상태 확인 프로토콜을 구현하지 않으므로 “구현되지 않음”(grpc_status=12)을 의미하는 상태 코드로 응답할 것으로 예상합니다. 그렇게 하면 활성 gRPC 서비스와 통신하고 있음을 나타내기에 충분합니다.

이 구성을 사용하면 지연이나 시간 초과가 발생하는 gRPC 클라이언트 없이 모든 백엔드 컨테이너를 중단할 수 있습니다. 활성 상태 확인은 NGINX Plus 전용입니다. 모범사례에서 gRPC 상태 확인에 대해 자세히 알아보세요.

6. 속도 제한 및 기타 API Gateway 제어(Control) 적용

grpc_gateway.conf의 샘플 구성은 TLS에 대한 약간의 수정을 포함하여 프로덕션 사용에 적합합니다. 패키지, 서비스 또는 RPC 방법을 기반으로 gRPC 요청을 라우팅하는 기능은 기존 NGINX 기능을 HTTP/REST API 또는 실제로 일반 웹 트래픽과 동일한 방식으로 gRPC 트래픽에 적용할 수 있음을 의미합니다. 각각의 경우에 관련 위치 블록은 속도 제한 또는 대역폭 제어와 같은 추가 구성으로 확장될 수 있습니다.

7. 요약

우리는 마이크로서비스 애플리케이션을 구축하기 위한 Cloud-Native 기술인 gRPC에 중점을 두었습니다. 

NGINX가 HTTP/REST API만큼 효과적으로 gRPC 애플리케이션을 제공할 수 있는 방법과 NGINX를 통해 다목적 API Gateway로 두 가지 API 스타일을 게시할 수 있는 방법을 제시하였습니다.

NGINX STORE 뉴스레터 및 최신 소식 구독하기

* indicates required