Lua en el servidor web: OpenResty, ngx_lua y scripting con Lua en nginx

nginx es uno de los servidores web más usados del mundo. Su modelo de workers asíncronos y no bloqueantes le permite manejar decenas de miles de conexiones simultáneas con una huella de memoria muy pequeña. La única pega es que su módulo de configuración no fue diseñado para lógica de negocio compleja. Ahí entra OpenResty: una distribución de nginx que integra LuaJIT dentro del proceso del servidor, permitiendo ejecutar código Lua con acceso completo a la petición HTTP y a las fases del ciclo de vida de nginx.

Qué es OpenResty

OpenResty no es un fork de nginx; es nginx compilado con un conjunto de módulos adicionales, el más importante de los cuales es ngx_lua (también llamado lua-nginx-module). Este módulo integra LuaJIT en el proceso de nginx y expone la API ngx.* para acceder a la petición, la respuesta, las variables de nginx y los subsistemas de conexión a servicios externos (Redis, MySQL, HTTP, DNS…). Cloudflare, Kong y muchas CDN lo usan en producción.

Bloques de Lua en la configuración de nginx

OpenResty añade directivas Lua a la sintaxis de nginx. Los bloques más habituales son:

  • init_by_lua_block: se ejecuta una vez al arrancar el master process. Útil para precargar módulos.
  • access_by_lua_block: se ejecuta antes de pasar la petición al upstream. Ideal para autenticación y rate limiting.
  • content_by_lua_block: genera la respuesta directamente desde Lua, sin upstream.
  • header_filter_by_lua_block: modifica las cabeceras de la respuesta.
  • log_by_lua_block: logging personalizado tras enviar la respuesta.
http {
    # Precargar módulos compartidos entre workers
    init_by_lua_block {
        cjson = require("cjson")
        redis = require("resty.redis")
    }

    server {
        listen 80;

        # Endpoint que responde directamente desde Lua
        location /api/saludo {
            content_by_lua_block {
                local nombre = ngx.var.arg_nombre or "mundo"
                ngx.header["Content-Type"] = "application/json"
                ngx.say(cjson.encode({
                    mensaje = "Hola, " .. nombre,
                    timestamp = ngx.time()
                }))
            }
        }

        # Autenticación con token antes de pasar al upstream
        location /privado/ {
            access_by_lua_block {
                local token = ngx.req.get_headers()["Authorization"]
                if not token or token ~= "Bearer secreto123" then
                    ngx.status = 401
                    ngx.say("No autorizado")
                    ngx.exit(401)
                end
            }
            proxy_pass http://backend;
        }
    }
}

La API ngx.*

La API que expone ngx_lua cubre prácticamente todo lo que se puede hacer con una petición HTTP:

-- Leer parámetros de la petición
local args = ngx.req.get_uri_args()          -- query string
local post  = ngx.req.get_post_args()        -- body form-urlencoded
local hdrs  = ngx.req.get_headers()          -- cabeceras
local metodo = ngx.req.get_method()          -- GET, POST...
local uri   = ngx.var.uri                    -- /ruta/actual
local ip    = ngx.var.remote_addr            -- IP del cliente

-- Leer el body raw (hay que llamar read_body antes)
ngx.req.read_body()
local body = ngx.req.get_body_data()

-- Responder
ngx.status = 200
ngx.header["X-Custom"] = "valor"
ngx.header["Content-Type"] = "text/plain"
ngx.say("Primera línea")
ngx.print("Sin salto de línea")

-- Redirigir
ngx.redirect("https://ejemplo.com", 302)

-- Terminar sin respuesta (útil en access_by_lua)
ngx.exit(ngx.HTTP_FORBIDDEN)

-- Logging
ngx.log(ngx.ERR, "Error inesperado: " .. tostring(err))
ngx.log(ngx.INFO, "Petición procesada en " .. ngx.var.request_time .. "s")

Conexión a Redis: cosocketAPI

La gran ventaja de OpenResty sobre un upstream tradicional es la cosocket API: operaciones de red no bloqueantes que usan corrutinas de Lua internamente. El código parece síncrono pero no bloquea el worker de nginx:

-- Rate limiting con Redis (en access_by_lua_block)
local redis = require("resty.redis")
local r = redis:new()
r:set_timeout(100)   -- 100ms de timeout

local ok, err = r:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "Redis no disponible: " .. err)
    return   -- dejar pasar si Redis está caído (fail open)
end

local ip = ngx.var.remote_addr
local key = "rl:" .. ip
local count, err = r:incr(key)
if count == 1 then
    r:expire(key, 60)   -- ventana de 60 segundos
end
r:close()   -- devolver la conexión al pool

if count > 100 then
    ngx.status = 429
    ngx.header["Retry-After"] = "60"
    ngx.say("Demasiadas peticiones")
    ngx.exit(429)
end

Kong API Gateway

Kong es el API gateway open source más popular, y está construido enteramente sobre OpenResty. Cada plugin de Kong es un módulo Lua que implementa alguna de las fases del ciclo de vida (access, header_filter, log…). Escribir un plugin personalizado es tan sencillo como crear una tabla Lua con los métodos de las fases que nos interesan:

-- Plugin Kong mínimo: añade una cabecera de respuesta
local MiPlugin = {}
MiPlugin.PRIORITY = 1000
MiPlugin.VERSION = "1.0.0"

function MiPlugin:header_filter(config)
    kong.response.set_header("X-Mi-Plugin", config.valor_cabecera)
end

return MiPlugin

OpenResty es una de las aplicaciones más interesantes de Lua en producción. Combina la eficiencia de nginx con la flexibilidad de un lenguaje de scripting sin sacrificar rendimiento, gracias a LuaJIT. Si quieres profundizar en las capacidades de LuaJIT más allá del contexto web, el artículo sobre LuaJIT y la FFI explica cómo alcanzar rendimiento cercano a C desde Lua.

Imagen: Pexels / Muhammed Ensar

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP