David - Musings of an SRE

Supercharge your Nginx with Openresty and Lua

Turn your Nginx into an API Gateway. Modify requests and responses from Nginx itself using Openresty and Lua.

Objectives

  • To deploy Openresty on a development/production environment
  • As a reverse proxy, to modify incoming request params and pass it on to another server
  • As a reverse proxy, to modify response from proxied server before returning to user

Openresty

Openresty is a open-sourced NGINX bundle that combines NGINX and various 3rd-party NGINX modules and Lua Libraries.

Openresty brings with it the power of the programming language Lua to write actual code directly on NGINX itself. Allowing NGINX administrators to add logic without having to write a seperate application to perform more complex routing activities.

Deploying Openresty

Thanks to Docker, there’s a quick way to get started with Openresty.

Depending on your prefered distribution, you can choose from a variety of supported distributions by the Openresty team.

The quickest way would be to copy the following:

$ docker run -p 80:80 openresty:trusty

For advanced users: Nginx configuration file is located in /usr/local/openresty/nginx/conf in your container. For a better experience, I’d recommend duplicating the openresty/nginx/conf directory locally and mounting it as a data volume so that you may change the nginx config from outside the container.

You can checkout Lomotif’s bootstrap.sh script that takes your container’s nginx/conf file and expose it outside.


Modifying your Nginx with Lua

Now that your Openresty is setup, lets get to the meaty part.

In your nginx.conf, you now have access to the following new directives:

access_by_lua, content_by_lua, init_by_lua and rewrite_by_lua.

Each of these directives also a _file, _block suffix available. Like access_by_lua_file, content_by_lua_block.

The official NGINX documentation has a pretty informative explaination on the differences.

Modifying incoming Requests

Lets say you are running a reverse proxy server that proxies all requests to another API server for processing.

You have certain parameters that you’d like to add/modify before passing that on.

How do we do that?

It should look something like:

# nginx.conf

server {
  listen 80;
  server_name yo.app.us;

  location / {
    access_by_lua_file /path/to/file.lua;
    proxy_pass http://url_to_proxy
  }
}
-- file.lua

function mask_content()
  nginx.req.read_body()
  local oldbody = ngx.req.get_body_data()
  local new_variables = string.gsum(oldbody, "string_to_change", "changed_string")
  ngx.req.set_body_data(new_variables)
end

if ngx.req.get_method() ~= "GET"
then
  mask_content()
end

Just with these changes, you can get nginx to read incoming parameters, make changes to it and then continue on and pass those same variables to the url defined in proxy_pass.

Note: We use access_by_lua_file and not content_by_lua_file because content_by_lua doesn’t allow proxy_pass to be in the same location block.

Modifying response from proxied server

With the proxy server sending new variables to the API server above, now we want to modify the response such as to remove any identification about where the actual API server lives or to remove sensitive data.

Modifying returned data is usually done on the content_by_lua directive which we would not have access to if we’re using proxy_pass.

So how do we do it?

We will have to modify our code a little bit and make use of a cool function called ngx.location.capture.

# nginx.conf

server {
  listen 80;
  server_name yo.app.us;

  location / {
    content_by_lua_file /path/to/content_change.lua;
  }

  location /legacy {
    access_by_lua_file /path/to/file.lua;
    proxy_pass http://url_to_proxy
  }
}

We can reuse the old file.lua.

-- file.lua

function mask_content()
  nginx.req.read_body()
  local oldbody = ngx.req.get_body_data()
  local new_variables = string.gsum(oldbody, "string_to_change", "changed_string")
  ngx.req.set_body_data(new_variables)
end

if ngx.req.get_method() ~= "GET"
then
  mask_content()
end

But lets create a new content_change.lua

-- content_change.lua

res = ngx.location.capture("/legacy")
local response_body = res.body

-- make changes to the response_body here

ngx.say(new_body)

And there you have it!

What’s next?

I’ve heard quite a bit about Lapis which is a Lua framework that runs inside of Openresty.

Some fun experiments for next week!

References