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:
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
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:
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:
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
And our lua
script:
lua/hello_world.lua
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.
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:
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:
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:
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:
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.
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.