gRPC + REST API on AWS
source link: https://ilya-sher.org/2023/04/21/grpc-rest-api-on-aws/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Showing a setup of gRPC service which is also exposed as a REST API. It’s a setup that happens to work for us. No alternatives will be discussed in this post.
This is a concise blog post.
Architecture
- ALB with HTTPS listener (trivially configured, out of scope of this post)
- ECS running a task with 3 containers:
Notes
Health checks are not in very good shape yet
ECS Configuration (Simplified Excerpt)
In case the reader is not familiar, it CloudFormation below.
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Name: apigw
Image: !Ref ApiGwImage
PortMappings:
- ContainerPort: !Ref ContainerPort
- Name: opa
Image: !Ref OpaImage
PortMappings:
- ContainerPort: 9191
- Name: app
Image: !Ref AppImage
PortMappings:
- ContainerPort: 4000
Service:
DependsOn:
- GrpcListenerRule
- RestListenerRule
- GrpcTargetGroup
- RestTargetGroup
Type: AWS::ECS::Service
Properties:
ServiceName: !Ref ServiceName
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
LoadBalancers:
- ContainerName: apigw
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref GrpcTargetGroup
- ContainerName: apigw
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref RestTargetGroup
GrpcTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: /
HealthCheckTimeoutSeconds: 5
Matcher:
GrpcCode: "0-99"
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Port: !Ref ContainerPort
Protocol: HTTP
ProtocolVersion: GRPC
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60 # default is 300
TargetType: ip
VpcId: !ImportValue VpcId
RestTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: /rest/not-found
HealthCheckTimeoutSeconds: 5
Matcher:
HttpCode: 404
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Port: !Ref ContainerPort
Protocol: HTTP
ProtocolVersion: HTTP1
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 60 # default is 300
TargetType: ip
VpcId: !ImportValue VpcId
GrpcListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref GrpcTargetGroup
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- '/censored.v1.CensoredService/*'
- '/censored.v1.CensoredAdminService/*'
- '/censored.v1.CensoredSystemService/*'
ListenerArn: ...
Priority: 1000
RestListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref RestTargetGroup
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- '/rest/v1/*'
ListenerArn: ...
Priority: 1001
Envoy Configuration (Simplified Excerpt)
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 8000
filter_chains:
- filters:
- name: Connection Manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
via: CensoredGW
route_config:
name: Static response for tests
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/test/static"
direct_response:
status: 200
body:
inline_string: "Static response for tests"
# Reference: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
- match:
prefix: "/"
route:
cluster: upstream
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
# maybe disable later:
auto_mapping: true
proto_descriptor: "../path/to/proto_descriptor.bin" ### See next heading in this post
services:
- censored.v1.CensoredService
- censored.v1.CensoredAdminService
- censored.v1.CensoredSystemService
print_options:
add_whitespace: true
always_print_primitive_fields: true
request_validation_options:
reject_unknown_method: true
reject_unknown_query_parameters: true
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
failure_mode_allow: false
with_request_body:
max_request_bytes: 10485760 # 10M
allow_partial_message: false
pack_as_bytes: true
transport_api_version: V3
grpc_service:
envoy_grpc:
cluster_name: opa-agent
timeout: 10s
- name: envoy.filters.http.router
# https://github.com/envoyproxy/envoy/issues/21464
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
always_set_request_id_in_response: true
access_log:
- typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
# https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-default-format
# Based on https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter
clusters:
- name: opa-agent
connect_timeout: 0.25s
type: STRICT_DNS
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: { }
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 9191
- name: upstream
type: STRICT_DNS
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: 127.0.0.1
port_value: 4000
proto_descriptor.bin
GrpcJsonTranscoder
must have the proto descriptor file in order to know how to transcode. The file contains:
- proto definitions of your services, including extension that describes how to expose the services as REST
- dependencies of the above proto definitions
The descriptor file is generated using a command similar to the following:
buf build -o proto_descriptor.bin --as-file-descriptor-set --path path/to/my.proto
buf is a way to manage .proto files and their dependencies (very imprecise definition, sorry)
If I remember correctly, you can generate the descriptor with protoc
(without buf
) but I don’t remember how.
grpcurl
Same descriptor file is used with grpcurl
when you later test your service from the command line:
grpcurl -H "Authorization: Bearer ..." -protoset proto_descriptor.bin "example.com:443" censored.service.name/MyFunc
my.proto
This is how a protobuf definition with REST extension looks like (excerpt):
import "google/api/annotations.proto";
service Censored {
rpc MyCreate(CreateRequest) returns (CreateResponse){
option (google.api.http) = { post: "/rest/v1/my-objs" };
}
rpc MyGet(GetRequest) returns (GetResponse) {
option (google.api.http) = { get: "/rest/v1/my-objs/{id}" };
}
}
Excerpt from buf.yaml
corresponding to the import
above:
version: v1
deps:
- buf.build/googleapis/googleapis
Hope this helps.
Sorry, I was in a rush to get this out. If anything is unclear or missing, please let me know.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK