2

Trying to setup an api gateway in Kubernetes with nginx. I am trying to follow the single subdomain pattern with the path specifying the service and version.

api.domain.com/service/v0/api/resource/10 -> http://servicev0/api/resource/10

Ignoring the version for now...

This does resolve.

location ~/(?<service>(\w+))/(?<version>(v[0-9]+(\.[0-9]+)*)) {
    resolver 169.254.169.250;
    proxy_pass http://theservice;
}

This does not resolve using api.domain/theservice/v0/

location ~/(?<service>(\w+))/(?<version>(v[0-9]+(\.[0-9]+)*)) {
    resolver 169.254.169.250;
    proxy_pass http://$service;
}

Error

*1 theservice could not be resolved (110: Operation timed out), 

Also need to rewrite request to drop the $service and $version.

alexander.polomodov
  • 1,060
  • 3
  • 10
  • 14
jpoehnelt
  • 121
  • 3

2 Answers2

1

As per my understanding the main problem in the question is DNS resolution of the variable in the proxy_pass directive.

Based on the official documentation and comments from Nginx support, only commercial version of Nginx supports dynamic resolution of the upstream. Free Nginx version only supports resolution on the start. So to update upstream IP address, you have to restart nginx process (it looks like reload does work as well).

The service variable isn't defined during the nginx process start. It brakes the resolution procedure and proxy_pass gets improper value which produce 502 error.

There are two possible ways to achieve similar to requested result, but with certain drawbacks.

Note: I've adjusted the regexp from the question sample to produce the path close to mentioned in the question:

api.domain.com/service/v0/api/resource/10 -> http://servicev0/api/resource/10

Details about the regexp can be obtained by following the link

1. Using upstream module

Drawback: you have to define upstreams for all possible services in advance, but you can adjust the path using regexp.

The following section added to the /etc/nginx/conf.d/default.conf allows to resolve the service name and send the proper path to the backend:

resolver 10.96.0.10;
upstream dynamic {
    server servicea0;
}

server {
...
    location ~ \/(?<service>\w+)\/v(?<version>[0-9]+(\.[0-9]+)*)(?<path>.*) {
        proxy_pass http://dynamic/$service$version$path;
    }

I've used default nginx container for testing. At this time it has version 1.19.5

To reload the nginx process configuration in the container I've exec into the Pod and run :

root@nginx1-74cf9547fb-8bldg:/# nginx -s reload

The following commands were also run inside the nginx container to skip network configuration details:

root@nginx1-74cf9547fb-8bldg:/# curl -v 127.0.0.1/serviceA/v0/api/resource/17
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET /serviceA/v0/api/resource/17 HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: nginx/1.19.5

# Respond from backend shows request headers
{
  "path": "/serviceA0/api/resource/17",
  "headers": {
    "host": "dynamic",
    "connection": "close",
    "user-agent": "curl/7.64.0",
    "accept": "*/*"
  },
  "method": "GET",
  "protocol": "http",

If service host name is defined in the proxy_pass directly, it sends request path without changes:

proxy_pass http://servicea0;

# Respond from backend shows request headers
{
  "path": "/serviceA/v0/api/resource/17",

If proxy_path specified using variables like the following, it also brakes the resolution procedure and proxy_pass gets improper value and returns 502 error.

proxy_pass http://servicea0/$service$version$path;

2. Using redirect

Drawback: It adds one more step and the second request will be initiated from the client, not from the reverse proxy, which limits its use cases, because of service discovery or DNS resolution issues. (e.g. for use inside the Kubernetes cluster only).

The following section was added to the /etc/nginx/conf.d/default.conf:

location ~ \/(?<service>\w+)\/v(?<version>[0-9]+(\.[0-9]+)*)(?<path>.*) {
    resolver 10.96.0.10;
    #proxy_pass http://$service$version$path;
    return 301 http://$service$version$path;
}

The following commands were also run inside the nginx Pod container:

root@nginx1-74cf9547fb-8bldg:/# curl -v 127.0.0.1/serviceA/v0/api/resource/10

* Expire in 0 ms for 6 (transfer 0x561d30fecf50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x561d30fecf50)
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET /serviceA/v0/api/resource/10 HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Server: nginx/1.19.5
< Date: Tue, 02 Mar 2021 16:45:12 GMT
< Content-Type: text/html
< Content-Length: 169
< Connection: keep-alive
< Location: http://serviceA0/api/resource/10    <--- redirect link looks as expected
< 
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.19.5</center>
</body>
</html>

I removed unimportant lines from the second test output:

root@nginx1-74cf9547fb-8bldg:/# curl -v 127.0.0.1/serviceC/v2.3.8/api/resource/12

* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET /serviceC/v2.3.8/api/resource/12 HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Server: nginx/1.19.5
< Location: http://serviceC2.3.8/api/resource/12
< 
<html>
<head><title>301 Moved Permanently</title></head>

How it may look like from the cluster microservice that supports redirects:
(curl -L - follow redirects)

root@nginx1-74cf9547fb-8bldg:/# curl -L 127.0.0.1/serviceA/v0/api/resource/17
{
  "path": "/api/resource/17",
  "headers": {
    "host": "serviceA0",
    "user-agent": "curl/7.64.0",
    "accept": "*/*"
  },
  "method": "GET",
  "body": "",
  "fresh": false,
  "hostname": "serviceA0",
  "ip": "::ffff:192.168.228.99",
  "ips": [],
  "protocol": "http",
  "query": {},
  "subdomains": [],
  "xhr": false,
  "os": {
    "hostname": "servicea0-66fcd9f788-2z4pk"
  },
  "connection": {}
}
VAS
  • 370
  • 1
  • 9
0

I suggest you look into the location format, good article here. However I've given you a direct answer at the bottom of this reply.

Most of my locations are in the format "location (modified) (string)", such as those below

location ~*  \.(jpg|jpeg|png|gif|css|js)$ {"
location = /wp-login.php {
location ~* (load_google_fonts|display_gallery_iframe) {
location ~ \.(hh|php)$ {
location ~*  "wp-content\/uploads\/(\d{4,}\/\d{2,}\/.*|galleries\/.*)" {

The only locations without a modifier are the ones for an exact match

location / {
location = /robots.txt {
location /favicon.ico {

I suspect you need something more like this - the only change is around the ~* and spaces right after "location". ~* is a case insensitive regular expression match.

location ~* /(?<service>(\w+))/(?<version>(v[0-9]+(\.[0-9]+)*)) {
Tim
  • 30,383
  • 6
  • 47
  • 77