How to Extend CubeEgress with Custom Lua Modules: 3 Methods Explained
You can extend CubeEgress by adding new Lua files to the OpenResty data plane, hooking code into the worker initialization phase, or registering custom policy action modules that integrate with the existing policy engine.
CubeEgress, the data-plane component of the CubeSandbox repository, runs on OpenResty (NGINX + LuaJIT) and stores its logic in standard Lua modules under CubeEgress/lua/. Because the architecture relies on Lua's require system rather than compiled plugins, you can inject custom business logic at three distinct lifecycle points without recompiling NGINX.
Three Methods for Extending CubeEgress with Custom Lua Modules
CubeEgress loads its functionality through ordinary Lua require statements executed during NGINX phases. This design yields three distinct extension strategies, each suited to different operational needs.
Option 1: Add Request-Level Logic via Phase Hooks
Drop a new .lua file alongside the existing egress modules and import it from one of the phase-specific entry points. The most common hooks are access_phase.lua (for request authorization), header_filter_phase.lua (for response manipulation), and log_phase.lua (for audit trails).
This method runs your code on every request entering the chosen phase. It is ideal for simple transformations like custom header injection, conditional redirects, or supplemental logging.
In CubeEgress/lua/access_phase.lua, the entry point loads the policy module with local policy = require "policy". You can add a similar line to load your custom module:
-- Add near the top of access_phase.lua
local my_custom = require "my_custom"
-- Invoke inside the request-handling loop
my_custom.handle()
Option 2: Worker-Level Initialization in init_worker_phase
Run code once per worker process by hooking into CubeEgress/lua/init_worker_phase.lua. This file triggers bootstrap.run() to load the policy bundle, making it the correct location for heavy-weight initialization that should not repeat per request.
Use this option to load static lookup tables, pre-compute data structures, start background timers, or register additional shared-dictionary keys.
-- Inside init_worker_phase.lua
local static_map = require "static_map"
static_map.load()
The init_worker_phase.lua file runs immediately after the worker spawns, before any traffic is processed, ensuring your data is ready before the first request arrives.
Option 3: Register Custom Policy Actions
Implement a module that conforms to the interface used by built-in actions (allow, inject, audit), then invoke it dynamically from access_phase.lua when a policy rule matches. This approach lets administrators toggle functionality via the JSON policy API without redeploying NGINX configuration files.
The policy-driven approach executes only when a specific rule matches, providing per-rule granularity. To implement, check for your custom module using pcall and pass the rule context:
-- Inside access_phase.lua rule execution block
local ok, custom_mod = pcall(require, "custom_action")
if ok then
custom_mod.apply(rule, ctx)
end
Your policy JSON can then reference the custom behavior:
{
"policy_id": "example",
"rules": [
{
"id": "r1",
"match": {},
"action": {
"allow": true,
"custom": "add_cookie"
}
}
]
}
Core Architecture and Entry Points
Understanding the Lua loading sequence helps you choose the correct hook point. CubeEgress uses the following files as primary extension anchors:
| File | Role | Key Loading Pattern |
|---|---|---|
CubeEgress/lua/init_worker_phase.lua |
Worker initialization | local bootstrap = require "bootstrap" |
CubeEgress/lua/bootstrap.lua |
Policy bundle download | Reads CUBE_EGRESS_BOOTSTRAP_URL environment variable |
CubeEgress/lua/access_phase.lua |
Request processing | local policy = require "policy" |
CubeEgress/lua/policy.lua |
Policy storage & validation | Interacts with ngx.shared.policy_store |
All custom modules follow the same pattern: create a Lua table, define functions, and return the table.
Implementation Examples
Example: Simple Request Header Injection
Create CubeEgress/lua/my_custom.lua to inject client IP headers:
local _M = {}
function _M.handle()
local ip = ngx.var.remote_addr
ngx.req.set_header("X-My-Proxy-Client", ip)
end
return _M
Hook it into access_phase.lua as shown in Option 1 above.
Example: Worker-Level Static Data Pre-loading
For data that should load once per worker:
-- CubeEgress/lua/static_map.lua
local cjson = require "cjson.safe"
local _M = { map = {} }
function _M.load()
local f = io.open("/etc/cube/lookup.json", "r")
if f then
local data = f:read("*a")
_M.map = cjson.decode(data) or {}
f:close()
end
end
return _M
Load this from init_worker_phase.lua to populate static_map.map before traffic hits.
Example: Policy-Driven Custom Actions
Implement a conditional cookie injector:
-- CubeEgress/lua/custom_action.lua
local _M = {}
function _M.apply(rule, ctx)
if rule.action.custom == "add_cookie" then
ngx.header["Set-Cookie"] = "mycookie=1; Path=/"
end
end
return _M
Register this in access_phase.lua using pcall(require, "custom_action") to safely load the module if present.
Shared Dictionary and State Management
CubeEgress stores runtime policies in ngx.shared.policy_store, defined in CubeEgress/lua/policy.lua. Custom modules can interact with this shared memory or declare their own dictionaries in the NGINX configuration (lua_shared_dict my_mod_store 10m).
The policy index is maintained manually through functions like read_index and with_index_lock in policy.lua. When extending CubeEgress, respect these locking patterns to avoid race conditions when updating shared state.
Summary
- CubeEgress uses standard Lua modules on OpenResty, allowing extensions via
requirestatements. - Option 1 (phase hooks) runs per-request in
access_phase.luaorheader_filter_phase.luafor header manipulation and logging. - Option 2 (
init_worker_phase) runs once per worker for static data loading and timer initialization. - Option 3 (policy actions) enables dynamic rule-based execution controlled via the JSON policy API.
- All extensions interact with shared dictionaries like
ngx.shared.policy_storeand follow the file paths inCubeEgress/lua/.
Frequently Asked Questions
Where do I place custom Lua modules in CubeEgress?
Place new .lua files in the CubeEgress/lua/ directory alongside the existing modules like bootstrap.lua and policy.lua. The OpenResty Lua package path includes this directory, so you can load them with require "my_custom" without additional path configuration.
Can I access the policy store from custom modules?
Yes. Custom modules can read and write to ngx.shared.policy_store, the same shared dictionary used by CubeEgress/lua/policy.lua. You can also define new shared dictionaries in the NGINX configuration for module-specific data isolation.
Do custom Lua modules require NGINX recompilation?
No. CubeEgress runs on LuaJIT through OpenResty, which interprets Lua modules at runtime. Simply drop your .lua files into the CubeEgress/lua/ directory and reload the NGINX configuration to pick up changes. No C compilation or binary linking is required.
How do I debug custom Lua code in CubeEgress?
Use ngx.log(ngx.ERR, "message") to write to the NGINX error log, or ngx.say() for development environments. Since the code runs in access_phase.lua or init_worker_phase.lua, standard OpenResty debugging techniques apply, including using pcall to catch module loading errors as implemented in the policy action extension pattern.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →