An openresty library provisioning dynamic (via JSON/API) load of lua code into nginx/openresty.
lua-resty-dynacode-ffmpeg.mp4
You can find the complete example in the usage
folder. The following steps will guide you through the basic usage:
Install the library: luarocks install resty-dynacode
Create a lua module to import and configure the library.
local dyna_controller = require "resty.dynacode.controller"
local controller = {} -- your module
dyna_controller.setup({
plugin_api_uri = "http://api:9090/response.json", -- the API providing the expected response
plugin_api_polling_interval = 15,
plugin_api_poll_at_init = true,
workers_max_jitter = 5,
shm = "cache_dict",
})
function controller.run()
dyna_controller.run()
end
return controller
And finally hooking up the phases at the nginx conf.
http {
# you must provide a shared memory space for caching
lua_shared_dict cache_dict 1m;
# spawning the pollers
init_worker_by_lua_block { require("controller").run() }
# hooking up all the phases (on http context)
rewrite_by_lua_block { require("controller").run() }
access_by_lua_block { require("controller").run() }
header_filter_by_lua_block { require("controller").run() }
body_filter_by_lua_block { require("controller").run() }
log_by_lua_block { require("controller").run() }
# the servers we want to add lua code
server {
listen 6060;
server_name dynamic.local.com;
location / {
content_by_lua_block { require("controller").run() }
}
}
}
Do what we already do with Lua, but without SIGHUP or deployment. It was inspired by a previous hackathon. Things this library enables you to do:
- Debug (log/metrify specific IP/token/user agent/cookie)
- Quick maneuvers:
- Block IP
- Deny requests per path/user agent/etc
- Drain a single server (302) / health check
- Turn on/off modules/variables
- ...
- Chaos testing
- Change any variables
- Modify response body
- Add response header (CORs, SCP, HSTS, X-Frame-Options, ...)
- Really anything you can do with lua/openresty
- in the background:
- start a poller
- fetch the JSON API response and save it to a shared memory
- compile (
loadstring
) the lua code and share it through each worker
- at the runtime (request cycle):
- select the proper domain (applying regex against current host)
- select the applicable plugins (based on phase/applicability)
- run them
graph LR
subgraph Nginx/Openresty Background
DynaCode -->|run each X seconds| Poller
Poller -->|already in cache| Cache[(Cache SHM)]
Poller -->|GET /plugins.json| Fetcher
Fetcher --> Cache
Cache --> Compiler[[Compiler]]
Compiler --> |share bytecode| LocalLuaVM([LocalLuaVM])
end
graph LR
subgraph Nginx/Openresty Request
Runner -->|library is| Enabled
Runner -->|host is not| Skippable
Runner -->|host matches| Regex
Runner -->|matches current| Phase
Runner -->|execute the function| LocalLuaVM([LocalLuaVM])
end
One can use events to expose metrics about the: poller
, fetcher
, caching
, compiler
, runner
, and etc.
You can create a CMS where you'll input your code, AKA plugins. A plugin belongs to a server/domain (*
, regex, etc), it has an nginx phase (access, rewrite, log, etc), and the lua code it represents. Your CMS then must expose these plugins in a known API/structure.
{
"general": {
"status": "enabled",
"skip_domains": [
"[\\\\w\\\\d\\\\.\\\\-]*server.local.com"
]
},
"domains": [
{
"name": "dynamic.local.com",
"plugins": [
{
"name": "dynamic content",
"code": "ngx.say(\"olá mundo!\")\r\nngx.say(\"hello world!\")",
"phase": "content"
},
{
"name": "adding cors headers",
"code": "ngx.header[\"Access-Control-Allow-Origin\"] = \"http://dynamic.local.com\"",
"phase": "header_filter"
},
{
"name": "authentication required",
"code": "local token = ngx.var.arg_token or ngx.var.cookie_superstition\r\n\r\nif token ~= 'token' then\r\n return ngx.exit(ngx.HTTP_FORBIDDEN)\r\nelse\r\n ngx.header['Set-Cookie'] = {'superstition=token'}\r\nend",
"phase": "access"
}
]
}
]
}
Once a JSON API is running, the openresty/nginx will fetch
regularly the plugins (in background), compile
them, and save them to cache. When a regular user issues a request then the runner
will see if the current context (server name, phase, etc.) matches with the plugin spec/requirements, and run it.
Although this library was made to support most of the failures types through pcall
, fallbacks
, and sensible defaults
you can't forget that a developer is still writing the code.
The following code will keep all nginx workers busy forever, effectively making it unreachable.
while true do print('The bullets, Just stop your crying') end
While one could try to solve that with quotas, but Luajit doesn't allow us to use that.
What happens when plugin API is offline? If the plugins are already in memory, that's fine. But when nginx was restarted/reloaded, it's going to "lose"
all the cached data.
- evaluate if having plugins separated from domains might be helpful (re-use among domains)
{
"general": {
},
"domains": [
{
"name": "dynamic.local.com",
"plugins": [1, 2]
}
]
"plugins": [
{
"id": 1,
"name": "dynamic content",
"code": "ngx.say(\"olá mundo!\")\r\nngx.say(\"hello world!\")",
"phase": "content"
},
{
"id": 2,
"name": "adding cors headers",
"code": "ngx.header[\"Access-Control-Allow-Origin\"] = \"http://dynamic.local.com\"",
"phase": "header_filter"
}
]
}
another way to have a plugin per multiple domains is to rely on
*
or regexes.*\.common.com
- CMS probably would benefit from having plugins code at a git repo (linked through its git path, therefore tested and developed like any other lua code already) and only render them at the response time
- measure the impact of lots of lua code being loaded (even though it's compressed), if there's any need to load the plugins per chunk/domain/whatever
- make CMS run a lua compile phase to avoid uncompiled code being deployed
- enable some way for user to setup the request for polling (providing authentication, and etc)
- avoid re-compilation when no plugins were altered (should we emit
BG_UPDATED_PLUGINS
or a new event) - review the events adding arguments when necessary/possible (for instance
BG_DIDNT_UPDATE_PLUGINS
) add a CMS for the complete exampleadd a quick start for the complete example- evaluate the lua-resty-mlcache rock to replace the current cache system
publish a rock- evaluate if an off-line mode makes sense (saving a local api response for -HUP/restart without link to API)
use / provide function direct access / local function instead of tables (ngx_now
,tbl.logger
)discuss the json format (making phases accessible without iterating through all plugins)offer events callbacks (like:on_compile_fail
,on_success
,...
)- maybe a vts plugin for metrics
testsdocumentation/drawing/use cases- build,
lint