Thursday, January 9, 2020

Using NGINX auth_request to proxy to dynamically multiple backend servers




Last week I've had to use NGINX as a reverse proxy for 2 microservices: backend A, and backend B.
However, I needed more than a simple reverse proxy.
Unlike the standard supported NGINX routing based on the request properties, such as URL and query string regex matching, I wanted to use a dynamic backend selection.

I want the NGINX to dynamically select a different backend server per request, based on a request to another microservice. The same request might be routed to the backend A microservice upon first access, but later might be required to be routed to backend server B. The decision was a complete responsibility of application code running in another microservice.



The expected flow is:

  1. The browser access the NGINX
  2. The NGINX sends a request to the router microservice
  3. The router microservice returns the current target for the request, for example: Backend A
  4. NGINX proxies the request to the related backend microservice: A or B
First, I thought about using NGINX lua, but I've found it not recommended for production environments, and quite complicated.

Then I've noticed the NGINX support for auth_request, especially this section in the NGINX documentation:


Syntax:auth_request_set $variable value;
Default:
Context:httpserverlocation
Sets the request variable to the given value after the authorization request completes. The value may contain variables from the authorization request, such as $upstream_http_*.


WOW!! Just what I need!
So, I've created the following NGINX configuration:

...

http {

  ...
  
  server {

    location /router
      internal;
      proxy_set_header originalHost $host;
      proxy_set_header originalUrl $request_uri;
      proxy_pass_request_body off;
      proxy_set_header Content-Length "";
      proxy_pass http://router-service/router
    }

    location / {
      auth_request /router;
      auth_request_set $validate_targetHost $upstream_http_targetHost;
      proxy_set_header Host $host;
      proxy_pass "${validate_targetHost}.default.svc.cluster.local${request_uri}";
    }
  }
}


The NGINX configuration contains 2 sections:

  1. The location /
    This includes a configuration to send an authorization request to the /router, get the targetHost header from the response, and use it to select the backend microservice as part of the proxy_pass direction.
  2. The location /router
    This include a configuration to send the request host and URL to the router microservice, without any body in the request.

The router service is a nodeJS express server.
Here is a snippet of the related code:


import express from 'express'
const app = express()

...


app.post('/router', (req, res) => {
  const originalUrl = req.header('originalUrl')
  const originalHost = req.header('originalHost')

  const targetHost = ...  // set targetHost to backendA or backendB based on application logic
  res.set('targetHost', targetHost)
  res.json({})
})

It looks like I've made it, but then I got an error from NGINX:

no resolver defined to resolve backendA while sending to client

Huh?
I've checked ping to the backendA service, and it works fine.
It turned out that NGINX is not used the standard DNS configuration (see here), so I have had to manually configure the DNS resolution for it.

First, I run the following script as part of NGINX startup:

echo resolver $(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf) ";" > /etc/nginx/resolvers.conf

and then, I've added the /etc/nginx/resolvers.conf to the nginx.conf file:

...

http {
   
  include         /etc/nginx/resolvers.conf;
  ...

And it worked!


Summary


This post covers how to use auth_request in NGINX to dynamically select the proxy backend.
We have presented how to implement a dynamic router microservice based on NodeJS express, and how to configure NGINX DNS resolvers.


1 comment:

  1. Thank you for your post! It greatly simplified the solution I was considering. Additionally the resolvers.conf was a time saver. On another note, the "NGINX lua" link you posted, has a message "Status Production ready." in its README.markdown file.

    ReplyDelete