Yos Riady software craftsman 🚀

Building an API Gateway with Lua and Nginx

Building an API Gateway with Lua and Nginx

When we work on microservices, there are often a number of common concerns / functionalities that should be shared amongst different services.

These common functionality include authentication, monitoring, logging, rate-limiting, IP whitelisting, and request transformations.

Instead of having each service verify their own request guarantees, it makes sense to offload these functionalities to a central gateway / proxy. This way, your engineering team is focused on building actual features/services and less boilerplate.

Most of the functionality of a service should be delegated to a proxy.

This pattern is often called the API Gateway.

Today, we’ll be building a simple API gateway from scratch. Alternatively, you can use some existing open source / commercial gateways from this curated list.

A Minimum Viable Gateway

For simplicity, we’ll work on just two core features:

  • Routing: We want to specify which services to forward requests to when a request hits a particular route at our gateway.

  • Request Transformation: We want to intercept and transform incoming requests prior to forwarding, so we can add additional functionalities such as authentication, rate limits, caching, etc.

Let’s get started!

Introducing OpenResty

Your API Gateway is the last thing you want to be a bottleneck. Since it’s the single entry point to your fleet of microservices, it’d better be up when the requests come in. To achieve low response times and high throughput, we turn to OpenResty.

OpenResty turns the NGINX server into a powerful web app server, in which developers can use the Lua programming language to script various existing nginx C modules and Lua modules and construct extremely high-performance web applications that are capable to handle 10K ~ 1000K+ connections in a single box.

With OpenResty, we can use Lua to script NGINX to do things that were only possible with NGINX configuration files.

Here are some more nice things about OpenResty.

You’ll need to install OpenResty on your machine to get started.

If you’re not familiar with Nginx, the beginner’s guide may be helpful.

But I don’t know Lua!

If you’ve written Ruby or Javascript, you should be able to pick it up in 15 minutes. Lua looks like this:

num = 42
s = 'walternate'

while num < 50 do
  num = num + 1
end

function fib(n)
  if n < 2 then return 1 end
  return fib(n - 2) + fib(n - 1)
end

t = {key1 = 'value1', key2 = false}

It’s a scripting language with fairly friendly syntax, and you should be alright just knowing the basics.

Running OpenResty locally

I’ve created a barebones OpenResty project you can just clone and run: openresty-quickstart

git clone git@github.com:yosriady/openresty-quickstart.git
cd openresty-quickstart
nginx -p /your/directory/openresty-quickstart -c conf/nginx.conf

Visit localhost:8080 to see a greeting from nginx.

An OpenResty introduction

At this point, you should have some basic familiarity with NGINX’s configuration file structure. NGINX’s consists of modules which are controlled by directives - a DSL - specified in the configuration file. Learn more here and here.

Here’s an example of an nginx .conf file:

#location is a block level directive

location / {
	#proxy_pass is a simple nginx directive
	proxy_pass https://localhost:5984/;
}

Openresty keeps the same structuring of the configuration files. You still create configuration files with simple and block level directives. Any nginx directive works with openresty in the same way as it would in a vanilla nginx application. In addition to that, OpenResty gives us additional directives which let us script behaviour with the lua language:

  • content_by_lua
  • init_by_lua
  • rewrite_by_lua
  • access_by_lua

We’ll go through each one, explaining what they do.

content_by_lua

The content_by_lua directive lets us run arbitrary lua code:

location / {
	content_by_lua 'ngx.say("<p>hello, world</p>")';
}

Running NGINX with the above configuration will execute the lua code specified at the root URL. In this case, we display an HTML element.

For serious projects with more complex logic, we can use content_by_lua_file:

conf/nginx.conf

location /by_file {
    default_type text/html;
    lua_code_cache off; #enables livereload for development
    content_by_lua_file ./lua/hello_world.lua;
}

And our lua script:

lua/hello_world.lua

ngx.say("<p>hello, world</p>");

Note that all four OpenResty directives listed above has a _file version that accepts a lua file path instead of a lua code block.

init_by_lua

The init_by_lua directive lets us run initialization code as the nginx server is starting up. One use of this directive is for importing and defining libraries or modules that are used in our request handlers.

init_by_lua '
cjson = require("cjson") -- cjson is a global variable
'

location / {
    content_by_lua '
    	local message = cjson.decode({hello="world"})
    	ngx.say(message)
    ';
}

In the above snippet, we initialize a library and assign it to a global variable that our request handlers can use.

We also use this directive to define some configuration constants for our gateway.

You can use init_by_lua_file for better code organization.

rewrite_by_lua

The rewrite_by_lua directive lets us ‘dynamically change the request URI using regular expressions, return redirects, and conditionally select configurations’.

In an API Gateway, this directive lets us route requests to its relevant destinations. For example, we can forward requests to /users to USER_MICROSERVICE_URL and forward requests to /assets to CDN_URL.

You can read more about how NGINX rewrite rules here.

access_by_lua

The access_by_lua directive lets us defines access policies for specific locations/addresses.

Our API Gateway uses this for handling HTTP authentication and IP blacklisting/whitelisting.

You can read more about the NGINX access module here.

OpenResty’s ngx package

You’ve already seen the ngx.say() method back in content_by_lua. say() is just one of the many methods defined on the ngx package, which is made available globally for other directives to freely use.

What else does ngx contain? Let’s take a look:

  • ngx.location.capture
  • ngx.req
  • ngx.resp

ngx.location.capture

Lets you make requests to an internal URI. Returns the response. For example:

local res = ngx.location.capture("/by_file")

The above code captures the response to the /by_file internal uri we’ve already defined somewhere in a location directive.

The res contains the status, header, and body of the response.

You can also pass arguments and other options in the URI:

local options = {
	method = ngx.HTTP_POST,
	args = { maxsize = 5000 }
}
local res = ngx.location.capture("/by_file", options)

ngx.req

We can modify the contents of the request before forwarding it to a destination server.

The ngx request object contains request attributes like so:

local headers = ngx.req.get_headers()
local cookie = headers["Cookie"]
local etag = headers["Etag"]
local host = headers["Host"]

local body = ngx.req.read_body()
local method = ngx.req.get_method
local querystring_params = ngx.req.get_uri_args()
local post_params = ngx.req.get_post_args()

All of the above request attributes can be modified or decorated with additional information, depending on your use case.

ngx.resp

We can modify the contents of the response before returning it to the client. For example, we can collate results from multiple services located at different internal addresses using ngx.location.capture, and then send it back to the client.

The ngx response object contains the following attributes:

local resp_headers = ngx.resp.get_headers()
local http_status = ngx.status

There is no ngx.res.body() method where you can set the body before sending the response.

Openresty instead offers two methods in ngx.say() and ngx.print() any argument to the methods will be joined and sent as the res body.

ngx.print("Hello world") --sends  Hello world
ngx.say("Hello world") -- sends Hello world/n that is the body appended with a newline
ngx.say(cjson.encode({a=1,b=2})) -- you can also send json in the response body

It is also important to note that calling any one of these methods means that the response will be sent back to the client. So the response headers and the response status that you have prepared up to this point will be sent back to the client once you call print() or say().

To be Continued!

In Part 2 of this series of blog posts on building API Gateways with Lua and NGINX, we’ll take a close look at how we can use the OpenResty directives we’ve seen to build a minimum viable API gateway.

Design Disclaimer

For simplicity, I’ve not used any Lua web frameworks for our gateway. However, for more complex production systems with more functionality it makes more sense to use Lapis instead. Lapis is a Lua web framework running on top of OpenResty. As a result, it ranks in the top 3 in recent performance benchmarks across all web frameworks.

Benchmarking

“He had acquired his belief not by honestly earning it in patient investigation, but by stifling his doubts.” – William K Clifford, 1874

I plan to do some proper performance tests & benchmarking using Siege, comparing our gateway with an alternative implementation in Node. Check back on this post at a later time.

Subscribe

Get notified of my latest articles by providing your email below.


Author

Hi, I'm Yos. Yos lives and works in Singapore building nifty software products.