Are you using an API gateway? Do you really need one? If you are using NixOS and feel comfortable with some Lua, you may want to consider OpenResty on NixOS as an API gateway.
An API Gateway is simply a service that acts as a reverse proxy for API requests to one or more upstream services. Modern API gateways streamline the management of many functionalities such as authentication, authorization, rate limiting, caching, logging, monitoring, etc. There are quite a few such proprietary and open-source API gateways available. Cloud service providers offer their own managed API gateways as well.
We have been using Apache APISIX for a while now. It is a high-performance, cloud-native API gateway solution. It also has a nice dashboard for managing APIs. However, I have been looking for a simpler and more portable solution for our use case. In particular, I want to be able manage the API gateway as a NixOS service so that the configuration can be tested and redeployed easily.
Two of the fairly popular open-source API gateways, Kong and Apache APISIX, are based on OpenResty that is mainly powered by Nginx and Lua. NixOS has first-class support for OpenResty. So, naturally, I decided to give it a try. The result was promising enough for us to start migrating our current APISIX deployments to OpenResty on NixOS.
In this post, I will show how to set up OpenResty as NixOS service along with a few rudimentary location examples.
We will test the NixOS configuration on a QEMU-based virtual machine. For this, I will use my short technical note on my blog, “Running NixOS Guests on QEMU”.
Configuration
Below is the configuration.nix file contents:
## file: ./configuration.nix { pkgs, ... }: let thisVersion = "0.0.1"; in { boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; networking.firewall.allowedTCPPorts = [ 22 80 ]; users.users.root = { initialPassword = "hebele-hubele"; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJIQtEmoHu44pUDwX5GEw20JLmfZaI+xVXin74GI396z" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdd2ubdTn5LPsN0zaxylrpkQTW+1Vr/uWQaEQXoGkd3" ]; }; services.openssh.enable = true; services.nginx = { enable = true; package = pkgs.openresty; virtualHosts."localhost" = { default = true; locations."/" = { return = "200 'Hello World!'"; }; locations."/version" = { extraConfig = '' default_type application/json; content_by_lua_block { ngx.say("{\"version\":\"${thisVersion}\"}") } ''; }; locations."/proxy" = { proxyPass = "http://httpbin.org/"; }; locations."/proxy/rewrite" = { proxyPass = "http://httpbin.org/"; extraConfig = '' header_filter_by_lua_block { if ngx.req.get_method() == "POST" and ngx.status == 200 then ngx.status = 201 end } ''; }; }; }; system.stateVersion = "24.05"; } Explanation
This configuration is pretty straightforward if you are familiar with NixOS and NixOS Nginx service options.
A few highlights:
- Instead of using the default Nginx package, we set
services.nginx.packagetopkgs.openresty. This will install OpenResty instead of Nginx. - The
/location is pretty much Nginx. - The
/proxylocation is proxying requests tohttpbin.org, again a typical Nginx configuration option. - The
/versionlocation is using Lua to return a JSON response with the version number. - The
/proxy/rewritelocation is proxying requests tohttpbin.organd changing the status code to201if the request method isPOST.
The last one is when OpenResty shines. You can use Lua to manipulate the request and response.
Note that the last example is actaully a production fix for us. We have a legacy service that returns 200 for all successful requests. But, the more recent versions of the service client requires an 201 response for successful POST requests.
Create and Launch VM
Let’s build the virtual machine with the following command:
nix-build '<nixpkgs/nixos>' -A vm -I nixpkgs=channel:nixos-24.05 -I nixos-config=./configuration.nix The built artifact can be found in the result symlink.
Then, launch the VM with run-nixos-vm script:
QEMU_KERNEL_PARAMS="console=ttyS0" QEMU_NET_OPTS="hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:24680-:80" ./result/bin/run-nixos-vm -nographic; reset … and test the configuration:
$ curl -D - http://localhost:24680 HTTP/1.1 200 OK Server: openresty Date: Thu, 01 Aug 2024 03:23:51 GMT Content-Type: application/octet-stream Content-Length: 12 Connection: keep-alive Hello World! Do we have a version?
$ curl -D - http://localhost:24680/version HTTP/1.1 200 OK Server: openresty Date: Thu, 01 Aug 2024 03:24:33 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: keep-alive {"version":"0.0.1"} Let’s proxy some requests:
$ curl -D - http://localhost:24680/proxy/anything/hello/world HTTP/1.1 200 OK Server: openresty Date: Thu, 01 Aug 2024 03:25:29 GMT Content-Type: application/json Content-Length: 355 Connection: keep-alive Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true { "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/8.7.1", "X-Amzn-Trace-Id": "Root=1-66ab002a-0ccb8633321d4b9563c6c599" }, "json": null, "method": "GET", "origin": "123.123.123.123", "url": "http://httpbin.org/anything/hello/world" } Let’s rewrite the response status upon successful POST requests:
$ curl -D - -X POST http://localhost:24680/proxy/rewrite/anything HTTP/1.1 201 Created Server: openresty Date: Thu, 01 Aug 2024 03:26:17 GMT Content-Type: application/json Content-Length: 344 Connection: keep-alive Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true { "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/8.7.1", "X-Amzn-Trace-Id": "Root=1-66ab0059-53f1b550657f07c10284f9e6" }, "json": null, "method": "POST", "origin": "123.123.123.123", "url": "http://httpbin.org/anything" } Note that the POST request to httpbin.org would return 200 status code:
curl -D - -X POST http://httpbin.org/anything HTTP/1.1 200 OK Date: Thu, 01 Aug 2024 03:27:18 GMT Content-Type: application/json Content-Length: 344 Connection: keep-alive Server: gunicorn/19.9.0 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true { "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/8.7.1", "X-Amzn-Trace-Id": "Root=1-66ab0096-7a3aa51e62f480b749d819f9" }, "json": null, "method": "POST", "origin": "123.123.123.123", "url": "http://httpbin.org/anything" } Final Remarks
I acknowledge the need for complex and expensive API gateway solutions for large and busy services. But, for small and medium-sized services, a simple API gateway can be sufficient.
OpenResty is a powerful tool that can be used for many purposes, potentially growing into a custom, in-house API gateway solution. Deploying OpenResty on NixOS gives us the ability to manage the configuration as code. This is an advantage for testing and reproducibility, and brings us one step closer to using OpenResty as an API gateway with endless possibilities.
Therefore, OpenResty on NixOS can be a good choice for both small and large service setups and teams. I like the idea that I invest into OpenResty and NixOS instead of a proprietary or particular solution.
But there can be some sharp edges in relation to NixOS. For example, I wanted to demonstrate how to use OpenResty for securing APIs with OpenID. Currently, both NixOS 24.05 and NixOS Unstable channels have broken lua-resty-openidc dependencies. It may not be a big deal for seasoned NixOS users, but it is a showstopper for newcomers and too much for a simple blog post.
Top comments (1)
Great read, very well explained!