T246379 resulted in a list of candidates for an API router, among them [[ https://www.envoyproxy.io | Envoy ]]. Since Envoy is already in use at the foundation, we should be particularly thorough in determining its feasibility. Where gaps exist, we should establish what sort of extensions would be required to satisfy requirements.
----
### Evaluation
#### Routing requests
The routes we require will generally take the form `api.wm.o/{something}/v{version}/{project}/{lang}/{path}` → `{lang}.{project}.org/{...}`; We need to parse language and project from the source URL, and use it to construct a destination hostname. Envoy has no native means of supporting this.
[[ https://github.com/eevans/wmf-api-gateway/blob/master/envoy.yaml | Our test configuration ]] uses the Lua HTTP filter to parse the URL, string format the destination hostname, and inject a new HTTP header, `X-Internal-Host`. During routing, [[ https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/route/route_components.proto.html#route-routeaction | auto_host_rewrite_header ]] is used to substitute the destination hostname with the value of `X-Internal-Host` (see snipet below).
```lang=yaml
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
safe_regex:
google_re2: {}
regex: "^/core/v\\d{1}/wikipedia/en/.*$"
route:
regex_rewrite:
pattern:
google_re2: {}
regex: ".*\/([^\/]+)\/wikipedia\/([^\/]+)(\/.*)$"
substitution: /w/rest.php/\1\3
auto_host_rewrite_header: "x-internal-host"
cluster: service_echoapi
- match:
prefix: "/"
route:
host_rewrite: www.google.com
cluster: service_echoapi
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
inline_code: |
function envoy_on_request(request_handle)
local path = request_handle:headers():get(":path")
project, lang = string.match(path, "^/%a+/v%d/(%a+)/(%a+)/.*$")
request_handle:headers():add("x-internal-host", lang .. "." ..project .. ".org")
end
- name: envoy.filters.http.router
```
This works, and despite seeming hacky, seems to be endorsed upstream.
It is not clear what the performance impact of using Lua to pattern match like this would be.
Any improvements would require coding (e.g. forking the filter if the changes cannot be pushed upstream, and/or creating a custom filter to perform the match).
#### Parsing rate limit values from JWT
In the context of an API router/rate-limiter, the JWT is a means of delivering a rate limit as an additional attribute of the payload. There is currently no requirement to perform authorization here; Authorization will be handled upstream. Envoy does have a [[https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter|JWT filter]], which unsurprisingly, is scoped as a means of performing authorization. It's not clear that this will be a problem, as long as the filter can be configured to validate the signature, and otherwise allow requests to pass, it could suffice.
[ ... ]
#### Rate limiting
##### What we want
Ideally, we parse the limit from an attribute of the JWT payload, and pass that to the rate-limiter.
##### What we have
Envoy has an HTTP filter for global rate limiting, it utilizes a rate limit service with a generic gRPC interface (as defined by [[ https://github.com/envoyproxy/envoy/blob/master/api/envoy/service/ratelimit/v3/rls.proto | rls.proto ]]). The RPC for this is something like:
```
ShouldRateLimit(RateLimitRequest) → RateLimitResponse
```
A `RateLimitRequest` has attributes `domain` (application namespacing), `descriptors`, and `hits_addend` (number of requests to count toward the limit; defaults to 1). The `descriptors` attribute is used to specify a list of identifiers, if any is over the limit, the request should be rejected.
Obviously, the assumption that is made here is that limit configuration is encapsulated in the rate limiter service, (and this is in fact how the [[ https://github.com/lyft/ratelimiter | reference implementation ]] works).
##### Logging
(IMPORTANT) TODO: Do.
----
See also: https://github.com/eevans/wmf-api-gateway — a test/dev environment using Docker Compose.